From 4bb85578a88d6ab6fa392b07d541a45639fb555e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20B=C3=A1lint=20Misius?= Date: Fri, 19 Jul 2019 02:53:54 +0200 Subject: [PATCH] Fix text wrapping (fixes #166) This breaks the syntax highlighting in ConsoleView. I haven't yet thought of a way to fix that properly, ideas anyone? --- src/graphics/Graphics.cpp | 65 ----- src/graphics/Graphics.h | 2 - src/gui/console/ConsoleView.cpp | 6 +- src/gui/interface/Label.cpp | 389 +++++++++--------------------- src/gui/interface/Label.h | 23 +- src/gui/interface/RichLabel.cpp | 45 +++- src/gui/interface/RichLabel.h | 1 + src/gui/interface/TextWrapper.cpp | 260 ++++++++++++++++++++ src/gui/interface/TextWrapper.h | 51 ++++ src/gui/interface/Textbox.cpp | 58 ++--- 10 files changed, 506 insertions(+), 394 deletions(-) create mode 100644 src/gui/interface/TextWrapper.cpp create mode 100644 src/gui/interface/TextWrapper.h diff --git a/src/graphics/Graphics.cpp b/src/graphics/Graphics.cpp index 17c08621f..5c14481f5 100644 --- a/src/graphics/Graphics.cpp +++ b/src/graphics/Graphics.cpp @@ -644,71 +644,6 @@ int Graphics::textwidthx(String str, int w) return n; } -int Graphics::PositionAtCharIndex(String str, int charIndex, int & positionX, int & positionY) -{ - int x = 0, y = 0, lines = 1; - String::value_type const *s = str.c_str(); - for (; *s; s++) - { - if (!charIndex) - break; - if(*s == '\n') { - lines++; - x = 0; - y += FONT_H; - charIndex--; - continue; - } else if(*s =='\b') { - if(!s[1]) break; - s++; - charIndex-=2; - continue; - } else if(*s == '\x0F') { - if(!s[1] || !s[2] || !s[3]) break; - s+=3; - charIndex-=4; - continue; - } - x += FontReader(*s).GetWidth(); - charIndex--; - } - positionX = x; - positionY = y; - return lines; -} - -int Graphics::CharIndexAtPosition(String str, int positionX, int positionY) -{ - int x=0, y=-2,charIndex=0,cw; - String::value_type const *s = str.c_str(); - for (; *s; s++) - { - if(*s == '\n') { - x = 0; - y += FONT_H; - charIndex++; - continue; - } else if(*s == '\b') { - if(!s[1]) break; - s++; - charIndex+=2; - continue; - } else if (*s == '\x0F') { - if(!s[1] || !s[2] || !s[3]) break; - s+=3; - charIndex+=4; - continue; - } - cw = FontReader(*s).GetWidth(); - if ((x+(cw/2) >= positionX && y+FONT_H >= positionY) || y > positionY) - break; - x += cw; - charIndex++; - } - return charIndex; -} - - int Graphics::textwrapheight(String str, int width) { int x=0, height=FONT_H, cw; diff --git a/src/graphics/Graphics.h b/src/graphics/Graphics.h index f58eef316..3b9c8a812 100644 --- a/src/graphics/Graphics.h +++ b/src/graphics/Graphics.h @@ -113,8 +113,6 @@ public: static pixel *render_packed_rgb(void *image, int width, int height, int cmp_size); //Font/text metrics - static int CharIndexAtPosition(String s, int positionX, int positionY); - static int PositionAtCharIndex(String s, int charIndex, int & positionX, int & positionY); static int CharWidth(String::value_type c); static int textnwidth(String s, int n); static void textnpos(String s, int n, int w, int *cx, int *cy); diff --git a/src/gui/console/ConsoleView.cpp b/src/gui/console/ConsoleView.cpp index 1c60dad91..958be631e 100644 --- a/src/gui/console/ConsoleView.cpp +++ b/src/gui/console/ConsoleView.cpp @@ -26,7 +26,7 @@ ConsoleView::ConsoleView(): CommandHighlighter(ConsoleView * v_) { v = v_; } void TextChangedCallback(ui::Textbox * sender) override { - sender->SetDisplayText(v->c->FormatCommand(sender->GetText())); + // sender->SetDisplayText(v->c->FormatCommand(sender->GetText())); } }; commandField = new ui::Textbox(ui::Point(0, Size.Y-16), ui::Point(Size.X, 16), ""); @@ -52,7 +52,7 @@ void ConsoleView::DoKeyPress(int key, int scan, bool repeat, bool shift, bool ct case SDLK_KP_ENTER: c->EvaluateCommand(commandField->GetText()); commandField->SetText(""); - commandField->SetDisplayText(""); + // commandField->SetDisplayText(""); break; case SDLK_DOWN: c->NextCommand(); @@ -106,7 +106,7 @@ void ConsoleView::NotifyPreviousCommandsChanged(ConsoleModel * sender) void ConsoleView::NotifyCurrentCommandChanged(ConsoleModel * sender) { commandField->SetText(sender->GetCurrentCommand().Command); - commandField->SetDisplayText(c->FormatCommand(commandField->GetText())); + // commandField->SetDisplayText(c->FormatCommand(commandField->GetText())); } diff --git a/src/gui/interface/Label.cpp b/src/gui/interface/Label.cpp index 8788dbd9e..72449591f 100644 --- a/src/gui/interface/Label.cpp +++ b/src/gui/interface/Label.cpp @@ -13,164 +13,53 @@ using namespace ui; Label::Label(Point position, Point size, String labelText): Component(position, size), - text(labelText), textColour(255, 255, 255), - selectionIndex0(-1), - selectionIndex1(-1), - selectionXL(-1), - selectionXH(-1), + selectionIndexL(textWrapper.IndexBegin()), + selectionIndexH(textWrapper.IndexBegin()), multiline(false), selecting(false), autoHeight(size.Y==-1?true:false) { + SetText(labelText); + menu = new ContextMenu(this); menu->AddItem(ContextMenuItem("Copy", 0, true)); } Label::~Label() { - } void Label::SetMultiline(bool status) { multiline = status; - if(status) - { - updateMultiline(); - updateSelection(); - TextPosition(textLines); - } - else - { - TextPosition(text); - } + updateTextWrapper(); + updateSelection(); + TextPosition(textWrapper.WrappedText()); } -void Label::SetText(String text) +void Label::SetText(String newText) { - this->text = text; - if(multiline) - { - updateMultiline(); - updateSelection(); - TextPosition(textLines); - } - else - { - TextPosition(text); - } + this->text = newText; + updateTextWrapper(); + updateSelection(); + TextPosition(textWrapper.WrappedText()); } void Label::AutoHeight() { bool oldAH = autoHeight; autoHeight = true; - updateMultiline(); + updateTextWrapper(); autoHeight = oldAH; } -void Label::updateMultiline() +void Label::updateTextWrapper() { - int lines = 1; - if (text.length()>0) + int lines = textWrapper.Update(text, multiline, Size.X - Appearance.Margin.Left - Appearance.Margin.Right); + if (autoHeight) { - String::value_type *rawText = new String::value_type[text.length()+1]; - std::copy(text.begin(), text.end(), rawText); - rawText[text.length()] = 0; - - String::value_type c, pc = 0; - int charIndex = 0; - - int wordWidth = 0; - int lineWidth = 0; - String::value_type *wordStart = NULL; - while ((c = rawText[charIndex++])) - { - switch(c) - { - case ' ': - lineWidth += Graphics::CharWidth(c); - lineWidth += wordWidth; - wordWidth = 0; - break; - case '\n': - lineWidth = wordWidth = 0; - lines++; - break; - default: - wordWidth += Graphics::CharWidth(c); - break; - } - if (pc == ' ') - { - wordStart = &rawText[charIndex-2]; - } - if ((c != ' ' || pc == ' ') && lineWidth + wordWidth >= Size.X-(Appearance.Margin.Left+Appearance.Margin.Right)) - { - if (wordStart && *wordStart) - { - *wordStart = '\n'; - if (lineWidth != 0) - lineWidth = wordWidth; - } - else if (!wordStart) - { - rawText[charIndex-1] = '\n'; - lineWidth = 0; - } - wordWidth = 0; - wordStart = 0; - lines++; - } - pc = c; - } - if (autoHeight) - { - Size.Y = lines*12+3; - } - textLines = rawText; - delete[] rawText; - /*int currentWidth = 0; - char * lastSpace = NULL; - char * currentWord = rawText; - char * nextSpace; - while(true) - { - nextSpace = strchr(currentWord+1, ' '); - if(nextSpace) - nextSpace[0] = 0; - int width = Graphics::textwidth(currentWord); - if(width+currentWidth >= Size.X-(Appearance.Margin.Left+Appearance.Margin.Right)) - { - currentWidth = width; - if(currentWord!=rawText) - { - currentWord[0] = '\n'; - lines++; - } - } - else - currentWidth += width; - if(nextSpace) - nextSpace[0] = ' '; - if(!currentWord[0] || !currentWord[1] || !(currentWord = strchr(currentWord+1, ' '))) - break; - } - if(autoHeight) - { - Size.Y = lines*12; - } - textLines = std::string(rawText); - delete[] rawText;*/ - } - else - { - if (autoHeight) - { - Size.Y = 15; - } - textLines = ""; + Size.Y = lines * 12 + 3; } } @@ -193,17 +82,17 @@ void Label::OnMouseClick(int x, int y, unsigned button) { if(button == SDL_BUTTON_RIGHT) { - if(menu) + if (menu) + { menu->Show(GetScreenPos() + ui::Point(x, y)); + } } else { selecting = true; - if(multiline) - selectionIndex0 = Graphics::CharIndexAtPosition(textLines, x-textPosition.X, y-textPosition.Y); - else - selectionIndex0 = Graphics::CharIndexAtPosition(text, x-textPosition.X, y-textPosition.Y); - selectionIndex1 = selectionIndex0; + selectionIndex0 = textWrapper.Point2Index(x - textPosition.X, y - textPosition.Y); + selectionIndexL = selectionIndex0; + selectionIndexH = selectionIndex0; updateSelection(); } @@ -211,18 +100,10 @@ void Label::OnMouseClick(int x, int y, unsigned button) void Label::copySelection() { - String currentText = text; - String copyText; - - if (selectionIndex1 > selectionIndex0) - copyText = currentText.Between(selectionIndex0, selectionIndex1).c_str(); - else if(selectionIndex0 > selectionIndex1) - copyText = currentText.Between(selectionIndex1, selectionIndex0).c_str(); - else if (!currentText.length()) - return; - else - copyText = currentText.c_str(); - ClipboardPush(format::CleanString(copyText, false, true, false).ToUtf8()); + if (HasSelection()) + { + ClipboardPush(format::CleanString(text.Between(selectionIndexL.raw_index, selectionIndexH.raw_index), false, true, false).ToUtf8()); + } } void Label::OnMouseUp(int x, int y, unsigned button) @@ -233,7 +114,9 @@ void Label::OnMouseUp(int x, int y, unsigned button) void Label::OnKeyPress(int key, int scan, bool repeat, bool shift, bool ctrl, bool alt) { if (repeat) + { return; + } if (ctrl && scan == SDL_SCANCODE_C) { copySelection(); @@ -247,19 +130,26 @@ void Label::OnKeyPress(int key, int scan, bool repeat, bool shift, bool ctrl, bo void Label::OnMouseMoved(int localx, int localy, int dx, int dy) { - if(selecting) + if (selecting) { - if(multiline) - selectionIndex1 = Graphics::CharIndexAtPosition(textLines, localx-textPosition.X, localy-textPosition.Y); + selectionIndex1 = textWrapper.Point2Index(localx - textPosition.X, localy - textPosition.Y); + if (selectionIndex1.raw_index < selectionIndex0.raw_index) + { + selectionIndexL = selectionIndex1; + selectionIndexH = selectionIndex0; + } else - selectionIndex1 = Graphics::CharIndexAtPosition(text, localx-textPosition.X, localy-textPosition.Y); + { + selectionIndexL = selectionIndex0; + selectionIndexH = selectionIndex1; + } updateSelection(); } } void Label::Tick(float dt) { - if(!this->IsFocused() && (selecting || (selectionIndex0 != -1 && selectionIndex1 != -1))) + if (!this->IsFocused() && (HasSelection() || selecting)) { ClearSelection(); } @@ -267,174 +157,123 @@ void Label::Tick(float dt) int Label::getLowerSelectionBound() { - return (selectionIndex0 > selectionIndex1) ? selectionIndex1 : selectionIndex0; + return selectionIndexL.raw_index; } int Label::getHigherSelectionBound() { - return (selectionIndex0 > selectionIndex1) ? selectionIndex0 : selectionIndex1; + return selectionIndexH.raw_index; } bool Label::HasSelection() { - if(selectionIndex0 != -1 && selectionIndex1 != -1 && selectionIndex0 != selectionIndex1) - return true; - return false; + return selectionIndexH.raw_index > selectionIndexL.raw_index; } void Label::ClearSelection() { selecting = false; - selectionIndex0 = -1; - selectionIndex1 = -1; + selectionIndexL = textWrapper.IndexBegin(); + selectionIndexH = textWrapper.IndexBegin(); updateSelection(); } void Label::selectAll() { - selectionIndex0 = 0; - selectionIndex1 = text.length(); + selectionIndexL = textWrapper.IndexBegin(); + selectionIndexH = textWrapper.IndexEnd(); updateSelection(); } void Label::updateSelection() { - String currentText; + if (selectionIndexL.raw_index < 0) selectionIndexL = textWrapper.IndexBegin(); + if (selectionIndexL.raw_index > (int)text.length()) selectionIndexL = textWrapper.IndexEnd(); + if (selectionIndexH.raw_index < 0) selectionIndexH = textWrapper.IndexBegin(); + if (selectionIndexH.raw_index > (int)text.length()) selectionIndexH = textWrapper.IndexEnd(); - if (selectionIndex0 < 0) selectionIndex0 = 0; - if (selectionIndex0 > (int)text.length()) selectionIndex0 = text.length(); - if (selectionIndex1 < 0) selectionIndex1 = 0; - if (selectionIndex1 > (int)text.length()) selectionIndex1 = text.length(); - - if(selectionIndex0 == -1 || selectionIndex1 == -1) + displayTextWithSelection = textWrapper.WrappedText(); + if (HasSelection()) { - selectionXH = -1; - selectionXL = -1; - - textFragments = currentText; - return; - } - - if(multiline) - currentText = textLines; - else - currentText = text; - - if(selectionIndex1 > selectionIndex0) { - selectionLineH = Graphics::PositionAtCharIndex(currentText, selectionIndex1, selectionXH, selectionYH); - selectionLineL = Graphics::PositionAtCharIndex(currentText, selectionIndex0, selectionXL, selectionYL); - - textFragments = currentText; - //textFragments.insert(selectionIndex1, "\x0E"); - //textFragments.insert(selectionIndex0, "\x0F\x01\x01\x01"); - textFragments.Insert(selectionIndex1, "\x01"); - textFragments.Insert(selectionIndex0, "\x01"); - } else if(selectionIndex0 > selectionIndex1) { - selectionLineH = Graphics::PositionAtCharIndex(currentText, selectionIndex0, selectionXH, selectionYH); - selectionLineL = Graphics::PositionAtCharIndex(currentText, selectionIndex1, selectionXL, selectionYL); - - textFragments = currentText; - //textFragments.insert(selectionIndex0, "\x0E"); - //textFragments.insert(selectionIndex1, "\x0F\x01\x01\x01"); - textFragments.Insert(selectionIndex0, "\x01"); - textFragments.Insert(selectionIndex1, "\x01"); - } else { - selectionXH = -1; - selectionXL = -1; - - textFragments = currentText; - } - - if(displayText.length()) - { - displayText = tDisplayText; - if(selectionIndex1 > selectionIndex0) { - int tSelectionIndex1 = Graphics::CharIndexAtPosition(displayText, selectionXH, selectionYH); - int tSelectionIndex0 = Graphics::CharIndexAtPosition(displayText, selectionXL, selectionYL); - - displayText.Insert(tSelectionIndex1, "\x01"); - displayText.Insert(tSelectionIndex0, "\x01"); - } else if(selectionIndex0 > selectionIndex1) { - int tSelectionIndex0 = Graphics::CharIndexAtPosition(displayText, selectionXH, selectionYH); - int tSelectionIndex1 = Graphics::CharIndexAtPosition(displayText, selectionXL, selectionYL); - - displayText.Insert(tSelectionIndex0, "\x01"); - displayText.Insert(tSelectionIndex1, "\x01"); - } + displayTextWithSelection.Insert(selectionIndexL.wrapped_index, "\x01"); + displayTextWithSelection.Insert(selectionIndexH.wrapped_index + 1, "\x01"); } } -void Label::SetDisplayText(String newText) -{ - ClearSelection(); - displayText = tDisplayText = newText; -} +// void Label::SetDisplayText(String newText) +// { + // displayText = newText; + // ClearSelection(); + // updateTextWrapper(); + // updateSelection(); + // TextPosition(textWrapper.WrappedText()); +// } void Label::Draw(const Point& screenPos) { - if(!drawn) + if (!drawn) { - if(multiline) - { - TextPosition(textLines); - updateMultiline(); - updateSelection(); - } - else - TextPosition(text); + TextPosition(textWrapper.WrappedText()); + updateTextWrapper(); + updateSelection(); drawn = true; } - Graphics * g = GetGraphics(); + Graphics *g = GetGraphics(); - String cDisplayText = displayText; + int selectionXL; + int selectionYL; + int selectionLineL = textWrapper.Index2Point(selectionIndexL, selectionXL, selectionYL); - if(!cDisplayText.length()) + int selectionXH; + int selectionYH; + int selectionLineH = textWrapper.Index2Point(selectionIndexH, selectionXH, selectionYH); + + if (HasSelection()) { - if(selectionXL != -1 && selectionXH != -1) + if (selectionLineH == selectionLineL) { - cDisplayText = textFragments; + g->fillrect( + screenPos.X + textPosition.X + selectionXL, + screenPos.Y + textPosition.Y + selectionYL - 1, + selectionXH - selectionXL, + 10, + 255, 255, 255, 255 + ); } else { - if(multiline) - cDisplayText = textLines; - else - cDisplayText = text; - } - } - - if(multiline) - { - if(selectionXL != -1 && selectionXH != -1) - { - if(selectionLineH - selectionLineL > 0) + g->fillrect( + screenPos.X + textPosition.X + selectionXL, + screenPos.Y + textPosition.Y + selectionYL - 1, + textSize.X - selectionXL, + 10, + 255, 255, 255, 255 + ); + for (int i = 1; i < selectionLineH - selectionLineL; ++i) { - g->fillrect(screenPos.X+textPosition.X+selectionXL, (screenPos.Y+textPosition.Y-1)+selectionYL, textSize.X-(selectionXL), 10, 255, 255, 255, 255); - for(int i = 1; i < selectionLineH-selectionLineL; i++) - { - g->fillrect(screenPos.X+textPosition.X, (screenPos.Y+textPosition.Y-1)+selectionYL+(i*12), textSize.X, 10, 255, 255, 255, 255); - } - g->fillrect(screenPos.X+textPosition.X, (screenPos.Y+textPosition.Y-1)+selectionYH, selectionXH, 10, 255, 255, 255, 255); - - } else { - g->fillrect(screenPos.X+textPosition.X+selectionXL, screenPos.Y+selectionYL+textPosition.Y-1, selectionXH-(selectionXL), 10, 255, 255, 255, 255); + g->fillrect( + screenPos.X + textPosition.X, + screenPos.Y + textPosition.Y + selectionYL - 1 + i * 12, + textSize.X, + 10, + 255, 255, 255, 255 + ); } - g->drawtext(screenPos.X+textPosition.X, screenPos.Y+textPosition.Y, cDisplayText, textColour.Red, textColour.Green, textColour.Blue, 255); - } - else - { - g->drawtext(screenPos.X+textPosition.X, screenPos.Y+textPosition.Y, cDisplayText, textColour.Red, textColour.Green, textColour.Blue, 255); - } - } else { - if(selectionXL != -1 && selectionXH != -1) - { - g->fillrect(screenPos.X+textPosition.X+selectionXL, screenPos.Y+textPosition.Y-1, selectionXH-(selectionXL), 10, 255, 255, 255, 255); - g->drawtext(screenPos.X+textPosition.X, screenPos.Y+textPosition.Y, cDisplayText, textColour.Red, textColour.Green, textColour.Blue, 255); - } - else - { - g->drawtext(screenPos.X+textPosition.X, screenPos.Y+textPosition.Y, cDisplayText, textColour.Red, textColour.Green, textColour.Blue, 255); + g->fillrect( + screenPos.X + textPosition.X, + screenPos.Y + textPosition.Y + selectionYH - 1, + selectionXH, + 10, + 255, 255, 255, 255 + ); } } + + g->drawtext( + screenPos.X + textPosition.X, + screenPos.Y + textPosition.Y, + displayTextWithSelection, + textColour.Red, textColour.Green, textColour.Blue, 255 + ); } diff --git a/src/gui/interface/Label.h b/src/gui/interface/Label.h index fa99a3ee6..10b6e4b43 100644 --- a/src/gui/interface/Label.h +++ b/src/gui/interface/Label.h @@ -5,6 +5,7 @@ #include "Component.h" #include "Colour.h" +#include "TextWrapper.h" namespace ui { @@ -12,32 +13,27 @@ namespace ui { protected: String textFragments; - String textLines; - String displayText; - String tDisplayText; + String displayTextWithSelection; String text; Colour textColour; - int selectionIndex0; - int selectionIndex1; - - int selectionXL; - int selectionXH; - int selectionYL; - int selectionYH; - int selectionLineL; - int selectionLineH; + TextWrapper::Index selectionIndex0; + TextWrapper::Index selectionIndex1; + TextWrapper::Index selectionIndexL; + TextWrapper::Index selectionIndexH; bool multiline; bool selecting; bool autoHeight; - void updateMultiline(); + void updateTextWrapper(); void updateSelection(); int getLowerSelectionBound(); int getHigherSelectionBound(); + TextWrapper textWrapper; + void copySelection(); public: //Label(Window* parent_state, String labelText); @@ -48,7 +44,6 @@ namespace ui void SetMultiline(bool status); virtual void SetText(String text); - virtual void SetDisplayText(String newText); virtual String GetText(); bool HasSelection(); diff --git a/src/gui/interface/RichLabel.cpp b/src/gui/interface/RichLabel.cpp index 471be0e07..613d76240 100644 --- a/src/gui/interface/RichLabel.cpp +++ b/src/gui/interface/RichLabel.cpp @@ -9,6 +9,7 @@ #include "gui/interface/Component.h" #include "graphics/Graphics.h" +#include "graphics/FontReader.h" #include "Colour.h" @@ -185,17 +186,49 @@ void RichLabel::Draw(const Point& screenPos) g->drawtext(screenPos.X+textPosition.X, screenPos.Y+textPosition.Y, displayText, textColour.Red, textColour.Green, textColour.Blue, 255); } +// don't ever use this for anything ever again eww +int EndMySuffering(String const &displayText, int positionX, int positionY) +{ + int x=0, y=-2,charIndex=0,cw; + auto s = displayText.begin(); + for (; s != displayText.end(); ++s) + { + if(*s == '\n') { + x = 0; + y += FONT_H; + charIndex++; + continue; + } else if(*s == '\b') { + if((displayText.end() - s) < 2) break; + s++; + charIndex+=2; + continue; + } else if (*s == '\x0F') { + if((displayText.end() - s) < 4) break; + s+=3; + charIndex+=4; + continue; + } + cw = FontReader(*s).GetWidth(); + if ((x+(cw/2) >= positionX && y+FONT_H >= positionY) || y > positionY) + break; + x += cw; + charIndex++; + } + return charIndex; +} + void RichLabel::OnMouseClick(int x, int y, unsigned button) { - int cursorPosition = Graphics::CharIndexAtPosition(displayText, x-textPosition.X, y-textPosition.Y); - for(std::vector::iterator iter = regions.begin(), end = regions.end(); iter != end; ++iter) + int cursorPosition = EndMySuffering(displayText, x-textPosition.X, y-textPosition.Y); + for (auto const ®ion : regions) { - if((*iter).start <= cursorPosition && (*iter).finish >= cursorPosition) + if (region.start <= cursorPosition && region.finish >= cursorPosition) { - switch((*iter).action) + switch (region.action) { - case 'a': - Platform::OpenURI((*iter).actionData.ToUtf8()); + case 'a': + Platform::OpenURI(region.actionData.ToUtf8()); break; } } diff --git a/src/gui/interface/RichLabel.h b/src/gui/interface/RichLabel.h index 14dd844df..ca586a893 100644 --- a/src/gui/interface/RichLabel.h +++ b/src/gui/interface/RichLabel.h @@ -26,6 +26,7 @@ namespace ui void Draw(const Point& screenPos) override; void OnMouseClick(int x, int y, unsigned button) override; + protected: String textSource; String displayText; diff --git a/src/gui/interface/TextWrapper.cpp b/src/gui/interface/TextWrapper.cpp new file mode 100644 index 000000000..17402fccf --- /dev/null +++ b/src/gui/interface/TextWrapper.cpp @@ -0,0 +1,260 @@ +#include "TextWrapper.h" + +#include "graphics/Graphics.h" +#include "graphics/FontReader.h" + +#include +#include +#include + +namespace ui +{ + int TextWrapper::Update(String const &text, bool do_wrapping, int max_width) + { + raw_text_size = (int)text.size(); + + struct wrap_record + { + String::value_type character; + int width; + std::iterator_traits::difference_type position; + bool wraps; + }; + int line_width = 0; + std::vector records; + + int word_begins_at = -1; // this is a pointer into records; we're not currently in a word + int word_width; + int lines = 1; + for (auto it = text.begin(); it != text.end(); ++it) + { + auto char_width = Graphics::CharWidth(*it); + + int sequence_length = 0; + switch (*it) // set sequence_length if *it starts a sequence that should be forwarded as-is + { + case '\b': sequence_length = 2; break; + case '\x0f': sequence_length = 4; break; + } + + switch (*it) + { + // add more supported spaces here + case ' ': + if (do_wrapping && line_width + char_width > max_width) + { + records.push_back(wrap_record{ + '\n', // character; makes the line wrap when rendered + 0, // width; fools the clickmap generator into not seeing this newline + 0, // position; the clickmap generator is fooled, this can be anything + true // signal the end of the line to the clickmap generator + }); + line_width = 0; + lines += 1; + } + else + { + // this is in an else branch to make spaces immediately following + // newline characters inserted by the wrapper disappear + records.push_back(wrap_record{ + *it, + char_width, + it - text.begin(), + false + }); + line_width += char_width; + } + word_begins_at = -1; // reset word state + break; + + // add more supported linebreaks here + case '\n': + records.push_back(wrap_record{ + *it, // character; makes the line wrap when rendered + max_width - line_width, // width; make it span all the way to the end + it - text.begin(), // position; so the clickmap generator knows where *it is + true // signal the end of the line to the clickmap generator + }); + lines += 1; + line_width = 0; + word_begins_at = -1; // reset word state + break; + + default: + if (sequence_length) // *it starts a sequence such as \b? or \x0f??? + { + if (text.end() - it < sequence_length) + { + it = text.end() - 1; + continue; // text is broken, we might as well skip the whole thing + } + for (auto skip = it + sequence_length; it != skip; ++it) + { + records.push_back(wrap_record{ + *it, // character; forward the sequence to the output + 0, // width; fools the clickmap generator into not seeing this sequence + 0, // position; the clickmap generator is fooled, this can be anything + false // signal nothing to the clickmap generator + }); + } + --it; + } + else + { + if (word_begins_at == -1) + { + word_begins_at = records.size(); + word_width = 0; + } + + if (do_wrapping && word_width + char_width > max_width) + { + records.push_back(wrap_record{ + '\n', // character; makes the line wrap when rendered + 0, // width; fools the clickmap generator into not seeing this newline + 0, // position; the clickmap generator is fooled, this can be anything + true // signal the end of the line to the clickmap generator + }); + lines += 1; + word_begins_at = records.size(); + word_width = 0; + line_width = 0; + } + if (do_wrapping && line_width + char_width > max_width) + { + // if we get in here, we skipped the previous block (since line_width + // would have been set to 0 (unless of course (char_width > max_width) which + // is dumb)). since (word_width + char_width) <= (line_width + char_width) always + // holds and we are in this block, we can be sure that word_width < line_width, + // so breaking the line by the preceding space is sure to decrease line_width. + records.insert(records.begin() + word_begins_at, wrap_record{ + '\n', // character; makes the line wrap when rendered + 0, // width; fools the clickmap generator into not seeing this newline + 0, // position; the clickmap generator is fooled, this can be anything + true // signal the end of the line to the clickmap generator + }); + lines += 1; + word_begins_at += 1; + line_width = word_width; + } + + records.push_back(wrap_record{ + *it, // character; make the line wrap with *it + char_width, // width; make it span all the way to the end + it - text.begin(), // position; so the clickmap generator knows where *it is + false // signal nothing to the clickmap generator + }); + word_width += char_width; + line_width += char_width; + } + break; + } + } + + regions.clear(); + wrapped_text.clear(); + int x = 0; + int l = 0; + int counter = 0; + for (auto const &record : records) + { + regions.push_back(clickmap_region{ x, l * FONT_H, record.width, l + 1, Index{ (int)record.position, counter } }); + ++counter; + x += record.width; + if (record.wraps) + { + x = 0; + l += 1; + } + wrapped_text.append(1, record.character); + } + + wrapped_lines = lines; + return lines; + } + + TextWrapper::Index TextWrapper::Point2Index(int x, int y) const + { + if (y < 0) + { + return IndexBegin(); + } + if (regions.size()) + { + auto curr = regions.begin(); + auto end = regions.end(); + + auto find_next_nonempty = [end](decltype(end) it) { + ++it; + while (it != end && !it->width) + { + ++it; + } + return it; + }; + + auto next = find_next_nonempty(curr); + while (next != end) + { + if (curr->pos_y + FONT_H > y) + { + if (curr->pos_x + curr->width / 2 > x) + { + // if x is to the left of the vertical bisector of the current region, + // return this one; really we should have returned 'the next one' in + // the previous iteration + return curr->index; + } + if (curr->pos_x + curr->width / 2 <= x && next->pos_x + next->width / 2 > x) + { + // if x is to the right of the vertical bisector of the current region + // but to the left of the next one's, return the next one + return next->index; + } + if (curr->pos_x + curr->width / 2 <= x && next->pos_y > curr->pos_y) + { + // nominate the next region if x is to the right of the vertical bisector of + // the current region and the next one is on a new line + return next->index; + } + } + curr = next; + next = find_next_nonempty(next); + } + } + return IndexEnd(); + } + + int TextWrapper::Index2Point(Index index, int &x, int &y) const + { + if (index.wrapped_index < 0 || index.wrapped_index > (int)regions.size() || !regions.size()) + { + return -1; + } + if (index.wrapped_index == (int)regions.size()) + { + x = regions[index.wrapped_index - 1].pos_x + regions[index.wrapped_index - 1].width; + y = regions[index.wrapped_index - 1].pos_y; + return regions[index.wrapped_index - 1].pos_line; + } + x = regions[index.wrapped_index].pos_x; + y = regions[index.wrapped_index].pos_y; + return regions[index.wrapped_index].pos_line; + } + + TextWrapper::Index TextWrapper::Raw2Index(int raw_index) const + { + if (raw_index < 0) + { + return IndexBegin(); + } + for (auto const ®ion : regions) + { + if (region.index.raw_index >= raw_index) + { + return region.index; + } + } + return IndexEnd(); + } +} diff --git a/src/gui/interface/TextWrapper.h b/src/gui/interface/TextWrapper.h new file mode 100644 index 000000000..e85b8f222 --- /dev/null +++ b/src/gui/interface/TextWrapper.h @@ -0,0 +1,51 @@ +#pragma once + +#include "common/String.h" +#include "Point.h" + +#include + +namespace ui +{ + class TextWrapper + { + public: + struct Index + { + int raw_index; + int wrapped_index; + }; + + private: + int raw_text_size; + String wrapped_text; + struct clickmap_region + { + int pos_x, pos_y, width, pos_line; + Index index; + }; + int wrapped_lines; + std::vector regions; + + public: + int Update(String const &text, bool do_wrapping, int max_width); + Index Raw2Index(int raw_index) const; + Index Point2Index(int x, int y) const; + int Index2Point(Index index, int &x, int &y) const; + + String const &WrappedText() const + { + return wrapped_text; + } + + Index IndexBegin() const + { + return Index{ 0, 0 }; + } + + Index IndexEnd() const + { + return Index{ raw_text_size, (int)wrapped_text.size() }; + } + }; +} diff --git a/src/gui/interface/Textbox.cpp b/src/gui/interface/Textbox.cpp index 94888ccde..2c11a7a27 100644 --- a/src/gui/interface/Textbox.cpp +++ b/src/gui/interface/Textbox.cpp @@ -77,7 +77,7 @@ void Textbox::SetText(String newText) if(cursor) { - Graphics::PositionAtCharIndex(multiline?textLines:text, cursor, cursorPositionX, cursorPositionY); + textWrapper.Index2Point(textWrapper.Raw2Index(cursor), cursorPositionX, cursorPositionY); } else { @@ -128,7 +128,7 @@ void Textbox::OnContextMenuAction(int item) void Textbox::resetCursorPosition() { - Graphics::PositionAtCharIndex(multiline?textLines:text, cursor, cursorPositionX, cursorPositionY); + textWrapper.Index2Point(textWrapper.Raw2Index(cursor), cursorPositionX, cursorPositionY); } void Textbox::TabFocus() @@ -169,14 +169,13 @@ void Textbox::cutSelection() text = backingText; } - if(multiline) - updateMultiline(); + updateTextWrapper(); updateSelection(); TextPosition(text); if(cursor) { - Graphics::PositionAtCharIndex(multiline?textLines:text, cursor, cursorPositionX, cursorPositionY); + textWrapper.Index2Point(textWrapper.Raw2Index(cursor), cursorPositionX, cursorPositionY); } else { @@ -210,12 +209,19 @@ void Textbox::pasteIntoSelection() if (!multiline && Graphics::textwidth(backingText + newText) > regionWidth) { int pLimit = regionWidth - Graphics::textwidth(backingText); - int cIndex = Graphics::CharIndexAtPosition(newText, pLimit, 0); - - if (cIndex > 0) - newText = newText.Substr(0, cIndex); - else - newText = ""; + int pWidth = 0; + auto it = newText.begin(); + while (it != newText.end()) + { + auto w = Graphics::CharWidth(*it); + if (pWidth + w > pLimit) + { + break; + } + pWidth += w; + ++it; + } + newText = String(newText.begin(), it); } backingText.Insert(cursor, newText); @@ -233,17 +239,13 @@ void Textbox::pasteIntoSelection() text = backingText; } - if(multiline) - updateMultiline(); + updateTextWrapper(); updateSelection(); - if(multiline) - TextPosition(textLines); - else - TextPosition(text); + TextPosition(textWrapper.WrappedText()); if(cursor) { - Graphics::PositionAtCharIndex(multiline?textLines:text, cursor, cursorPositionX, cursorPositionY); + textWrapper.Index2Point(textWrapper.Raw2Index(cursor), cursorPositionX, cursorPositionY); } else { @@ -455,17 +457,13 @@ void Textbox::AfterTextChange(bool changed) } } - if (multiline) - updateMultiline(); + updateTextWrapper(); updateSelection(); - if (multiline) - TextPosition(textLines); - else - TextPosition(text); + TextPosition(textWrapper.WrappedText()); if(cursor) { - Graphics::PositionAtCharIndex(multiline?textLines:text, cursor, cursorPositionX, cursorPositionY); + textWrapper.Index2Point(textWrapper.Raw2Index(cursor), cursorPositionX, cursorPositionY); } else { @@ -515,10 +513,11 @@ void Textbox::OnMouseClick(int x, int y, unsigned button) if (button != SDL_BUTTON_RIGHT) { mouseDown = true; - cursor = Graphics::CharIndexAtPosition(multiline?textLines:text, x-textPosition.X, y-textPosition.Y); + auto index = textWrapper.Point2Index(x-textPosition.X, y-textPosition.Y); + cursor = index.raw_index; if(cursor) { - Graphics::PositionAtCharIndex(multiline?textLines:text, cursor, cursorPositionX, cursorPositionY); + textWrapper.Index2Point(index, cursorPositionX, cursorPositionY); } else { @@ -538,10 +537,11 @@ void Textbox::OnMouseMoved(int localx, int localy, int dx, int dy) { if(mouseDown) { - cursor = Graphics::CharIndexAtPosition(multiline?textLines:text, localx-textPosition.X, localy-textPosition.Y); + auto index = textWrapper.Point2Index(localx-textPosition.X, localy-textPosition.Y); + cursor = index.raw_index; if(cursor) { - Graphics::PositionAtCharIndex(multiline?textLines:text, cursor, cursorPositionX, cursorPositionY); + textWrapper.Index2Point(index, cursorPositionX, cursorPositionY); } else {