Index: ps/trunk/source/gui/CGUIText.cpp =================================================================== --- ps/trunk/source/gui/CGUIText.cpp (revision 25533) +++ ps/trunk/source/gui/CGUIText.cpp (revision 25534) @@ -1,472 +1,478 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "CGUIText.h" #include "graphics/FontMetrics.h" #include "graphics/ShaderManager.h" #include "graphics/TextRenderer.h" #include "gui/CGUI.h" #include "gui/ObjectBases/IGUIObject.h" #include "gui/SettingTypes/CGUIString.h" #include "ps/CStrInternStatic.h" #include "renderer/Renderer.h" #include extern int g_xres, g_yres; extern float g_GuiScale; // TODO Gee: CRect => CPoint ? void SGenerateTextImage::SetupSpriteCall( const bool Left, CGUIText::SSpriteCall& SpriteCall, const float width, const float y, const CSize2D& Size, const CStr& TextureName, const float BufferZone) { // TODO Gee: Temp hardcoded values SpriteCall.m_Area.top = y + BufferZone; SpriteCall.m_Area.bottom = y + BufferZone + Size.Height; if (Left) { SpriteCall.m_Area.left = BufferZone; SpriteCall.m_Area.right = Size.Width + BufferZone; } else { SpriteCall.m_Area.left = width-BufferZone - Size.Width; SpriteCall.m_Area.right = width-BufferZone; } SpriteCall.m_Sprite = TextureName; m_YFrom = SpriteCall.m_Area.top - BufferZone; m_YTo = SpriteCall.m_Area.bottom + BufferZone; m_Indentation = Size.Width + BufferZone * 2; } CGUIText::CGUIText(const CGUI& pGUI, const CGUIString& string, const CStrW& FontW, const float Width, const float BufferZone, const EAlign align, const IGUIObject* pObject) { if (string.m_Words.empty()) return; CStrIntern Font(FontW.ToUTF8()); float x = BufferZone, y = BufferZone; // drawing pointer int from = 0; bool FirstLine = true; // Necessary because text in the first line is shorter // (it doesn't count the line spacing) // Images on the left or the right side. SGenerateTextImages Images; int pos_last_img = -1; // Position in the string where last img (either left or right) were encountered. // in order to avoid duplicate processing. // Go through string word by word for (int i = 0; i < static_cast(string.m_Words.size()) - 1; ++i) { // Pre-process each line one time, so we know which floating images // will be added for that line. // Generated stuff is stored in Feedback. CGUIString::SFeedback Feedback; // Preliminary line height, used for word-wrapping with floating images. float prelim_line_height = 0.f; // Width and height of all text calls generated. string.GenerateTextCall(pGUI, Feedback, Font, string.m_Words[i], string.m_Words[i+1], FirstLine); SetupSpriteCalls(pGUI, Feedback.m_Images, y, Width, BufferZone, i, pos_last_img, Images); pos_last_img = std::max(pos_last_img, i); x += Feedback.m_Size.Width; prelim_line_height = std::max(prelim_line_height, Feedback.m_Size.Height); // If Width is 0, then there's no word-wrapping, disable NewLine. if (((Width != 0 && (x > Width - BufferZone || Feedback.m_NewLine)) || i == static_cast(string.m_Words.size()) - 2) && ProcessLine(pGUI, string, Font, pObject, Images, align, prelim_line_height, Width, BufferZone, FirstLine, x, y, i, from)) return; } } // Loop through our images queues, to see if images have been added. void CGUIText::SetupSpriteCalls( const CGUI& pGUI, const std::array, 2>& FeedbackImages, const float y, const float Width, const float BufferZone, const int i, const int pos_last_img, SGenerateTextImages& Images) { // Check if this has already been processed. // Also, floating images are only applicable if Word-Wrapping is on if (Width == 0 || i <= pos_last_img) return; // Loop left/right for (int j = 0; j < 2; ++j) for (const CStr& imgname : FeedbackImages[j]) { SSpriteCall SpriteCall; SGenerateTextImage Image; // Y is if no other floating images is above, y. Else it is placed // after the last image, like a stack downwards. float _y; if (!Images[j].empty()) _y = std::max(y, Images[j].back().m_YTo); else _y = y; const SGUIIcon& icon = pGUI.GetIcon(imgname); Image.SetupSpriteCall(j == CGUIString::SFeedback::Left, SpriteCall, Width, _y, icon.m_Size, icon.m_SpriteName, BufferZone); // Check if image is the lowest thing. m_Size.Height = std::max(m_Size.Height, Image.m_YTo); Images[j].emplace_back(Image); m_SpriteCalls.emplace_back(std::move(SpriteCall)); } } // Now we'll do another loop to figure out the height and width of // the line (the height of the largest character and the width is // the sum of all of the individual widths). This // couldn't be determined in the first loop (main loop) // because it didn't regard images, so we don't know // if all characters processed, will actually be involved // in that line. void CGUIText::ComputeLineSize( const CGUI& pGUI, const CGUIString& string, const CStrIntern& Font, const bool FirstLine, const float Width, const float width_range_to, const int i, const int temp_from, float& x, CSize2D& line_size) const { for (int j = temp_from; j <= i; ++j) { // We don't want to use Feedback now, so we'll have to use another one. CGUIString::SFeedback Feedback2; // Don't attach object, it'll suppress the errors // we want them to be reported in the final GenerateTextCall() // so that we don't get duplicates. string.GenerateTextCall(pGUI, Feedback2, Font, string.m_Words[j], string.m_Words[j+1], FirstLine); // Append X value. x += Feedback2.m_Size.Width; if (Width != 0 && x > width_range_to && j != temp_from && !Feedback2.m_NewLine) { // The calculated width of each word includes the space between the current // word and the next. When we're wrapping, we need subtract the width of the // space after the last word on the line before the wrap. CFontMetrics currentFont(Font); line_size.Width -= currentFont.GetCharacterWidth(*L" "); break; } // Let line_size.cy be the maximum m_Height we encounter. line_size.Height = std::max(line_size.Height, Feedback2.m_Size.Height); // If the current word is an explicit new line ("\n"), // break now before adding the width of this character. // ("\n" doesn't have a glyph, thus is given the same width as // the "missing glyph" character by CFont::GetCharacterWidth().) if (Width != 0 && Feedback2.m_NewLine) break; line_size.Width += Feedback2.m_Size.Width; } } bool CGUIText::ProcessLine( const CGUI& pGUI, const CGUIString& string, const CStrIntern& Font, const IGUIObject* pObject, const SGenerateTextImages& Images, const EAlign align, const float prelim_line_height, const float Width, const float BufferZone, bool& FirstLine, float& x, float& y, int& i, int& from) { // Change 'from' to 'i', but first keep a copy of its value. int temp_from = from; from = i; float width_range_from = BufferZone; float width_range_to = Width - BufferZone; ComputeLineRange(Images, y, Width, prelim_line_height, width_range_from, width_range_to); // Reset X for the next loop x = width_range_from; CSize2D line_size; ComputeLineSize(pGUI, string, Font, FirstLine, Width, width_range_to, i, temp_from, x, line_size); // Reset x once more x = width_range_from; // Move down, because font drawing starts from the baseline y += line_size.Height; const float dx = GetLineOffset(align, width_range_from, width_range_to, line_size); // Do the real processing now const bool done = AssembleCalls(pGUI, string, Font, pObject, FirstLine, Width, width_range_to, dx, y, temp_from, i, x, from); // Reset X x = BufferZone; // Update dimensions m_Size.Width = std::max(m_Size.Width, line_size.Width + BufferZone * 2); m_Size.Height = std::max(m_Size.Height, y + BufferZone); FirstLine = false; // Now if we entered as from = i, then we want // i being one minus that, so that it will become // the same i in the next loop. The difference is that // we're on a new line now. i = from - 1; return done; } // Decide width of the line. We need to iterate our floating images. // this won't be exact because we're assuming the line_size.cy // will be as our preliminary calculation said. But that may change, // although we'd have to add a couple of more loops to try straightening // this problem out, and it is very unlikely to happen noticeably if one // structures his text in a stylistically pure fashion. Even if not, it // is still quite unlikely it will happen. // Loop through left and right side, from and to. void CGUIText::ComputeLineRange( const SGenerateTextImages& Images, const float y, const float Width, const float prelim_line_height, float& width_range_from, float& width_range_to) const { // Floating images are only applicable if word-wrapping is enabled. if (Width == 0) return; for (int j = 0; j < 2; ++j) for (const SGenerateTextImage& img : Images[j]) { // We're working with two intervals here, the image's and the line height's. // let's find the union of these two. float union_from, union_to; union_from = std::max(y, img.m_YFrom); union_to = std::min(y + prelim_line_height, img.m_YTo); // The union is not empty if (union_to > union_from) { if (j == 0) width_range_from = std::max(width_range_from, img.m_Indentation); else width_range_to = std::min(width_range_to, Width - img.m_Indentation); } } } // compute offset based on what kind of alignment float CGUIText::GetLineOffset( const EAlign align, const float width_range_from, const float width_range_to, const CSize2D& line_size) const { switch (align) { case EAlign::LEFT: // don't add an offset return 0.f; case EAlign::CENTER: return ((width_range_to - width_range_from) - line_size.Width) / 2; case EAlign::RIGHT: return width_range_to - line_size.Width; default: debug_warn(L"Broken EAlign in CGUIText()"); return 0.f; } } bool CGUIText::AssembleCalls( const CGUI& pGUI, const CGUIString& string, const CStrIntern& Font, const IGUIObject* pObject, const bool FirstLine, const float Width, const float width_range_to, const float dx, const float y, const int temp_from, const int i, float& x, int& from) { bool done = false; for (int j = temp_from; j <= i; ++j) { // We don't want to use Feedback now, so we'll have to use another one. CGUIString::SFeedback Feedback2; // Defaults string.GenerateTextCall(pGUI, Feedback2, Font, string.m_Words[j], string.m_Words[j+1], FirstLine, pObject); // Iterate all and set X/Y values // Since X values are not set, we need to make an internal // iteration with an increment that will append the internal // x, that is what x_pointer is for. float x_pointer = 0.f; for (STextCall& tc : Feedback2.m_TextCalls) { tc.m_Pos = CVector2D(dx + x + x_pointer, y); x_pointer += tc.m_Size.Width; if (tc.m_pSpriteCall) tc.m_pSpriteCall->m_Area += tc.m_Pos - CSize2D(0, tc.m_pSpriteCall->m_Area.GetHeight()); } // Append X value. x += Feedback2.m_Size.Width; // The first word overrides the width limit, what we // do, in those cases, are just drawing that word even // though it'll extend the object. if (Width != 0) // only if word-wrapping is applicable { if (Feedback2.m_NewLine) { from = j + 1; // Sprite call can exist within only a newline segment, // therefore we need this. if (!Feedback2.m_SpriteCalls.empty()) { auto newEnd = std::remove_if(Feedback2.m_TextCalls.begin(), Feedback2.m_TextCalls.end(), [](const STextCall& call) { return !call.m_pSpriteCall; }); m_TextCalls.insert( m_TextCalls.end(), std::make_move_iterator(Feedback2.m_TextCalls.begin()), std::make_move_iterator(newEnd)); m_SpriteCalls.insert( m_SpriteCalls.end(), std::make_move_iterator(Feedback2.m_SpriteCalls.begin()), std::make_move_iterator(Feedback2.m_SpriteCalls.end())); } break; } else if (x > width_range_to && j == temp_from) { from = j+1; // do not break, since we want it to be added to m_TextCalls } else if (x > width_range_to) { from = j; break; } } // Add the whole Feedback2.m_TextCalls to our m_TextCalls. m_TextCalls.insert( m_TextCalls.end(), std::make_move_iterator(Feedback2.m_TextCalls.begin()), std::make_move_iterator(Feedback2.m_TextCalls.end())); m_SpriteCalls.insert( m_SpriteCalls.end(), std::make_move_iterator(Feedback2.m_SpriteCalls.begin()), std::make_move_iterator(Feedback2.m_SpriteCalls.end())); if (j == static_cast(string.m_Words.size()) - 2) done = true; } return done; } -void CGUIText::Draw(CGUI& pGUI, const CGUIColor& DefaultColor, const CVector2D& pos, const CRect& clipping) const +void CGUIText::Draw(CGUI& pGUI, const CGUIColor& DefaultColor, const CVector2D& pos, CRect clipping) const { CShaderTechniquePtr tech = g_Renderer.GetShaderManager().LoadEffect(str_gui_text); tech->BeginPass(); bool isClipped = clipping != CRect(); if (isClipped) { + // Make clipping rect as small as possible to prevent rounding errors + clipping.top = std::ceil(clipping.top); + clipping.bottom = std::floor(clipping.bottom); + clipping.left = std::ceil(clipping.left); + clipping.right = std::floor(clipping.right); + glEnable(GL_SCISSOR_TEST); glScissor( - clipping.left * g_GuiScale, - g_yres - clipping.bottom * g_GuiScale, - clipping.GetWidth() * g_GuiScale, - clipping.GetHeight() * g_GuiScale); + std::ceil(clipping.left * g_GuiScale), + std::ceil(g_yres - clipping.bottom * g_GuiScale), + std::floor(clipping.GetWidth() * g_GuiScale), + std::floor(clipping.GetHeight() * g_GuiScale)); } CTextRenderer textRenderer(tech->GetShader()); textRenderer.SetClippingRect(clipping); textRenderer.Translate(0.0f, 0.0f, 0.0f); for (const STextCall& tc : m_TextCalls) { // If this is just a placeholder for a sprite call, continue if (tc.m_pSpriteCall) continue; textRenderer.Color(tc.m_UseCustomColor ? tc.m_Color : DefaultColor); textRenderer.Font(tc.m_Font); textRenderer.Put(floorf(pos.X + tc.m_Pos.X), floorf(pos.Y + tc.m_Pos.Y), &tc.m_String); } textRenderer.Render(); for (const SSpriteCall& sc : m_SpriteCalls) pGUI.DrawSprite(sc.m_Sprite, sc.m_Area + pos); if (isClipped) glDisable(GL_SCISSOR_TEST); tech->EndPass(); } Index: ps/trunk/source/gui/CGUIText.h =================================================================== --- ps/trunk/source/gui/CGUIText.h (revision 25533) +++ ps/trunk/source/gui/CGUIText.h (revision 25534) @@ -1,281 +1,281 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_GUITEXT #define INCLUDED_GUITEXT #include "gui/CGUISprite.h" #include "gui/SettingTypes/CGUIColor.h" #include "gui/SettingTypes/EAlign.h" #include "maths/Rect.h" #include "maths/Size2D.h" #include "maths/Vector2D.h" #include "ps/CStrIntern.h" #include #include #include class CGUI; class CGUIString; class IGUIObject; struct SGenerateTextImage; using SGenerateTextImages = std::array, 2>; /** * An CGUIText object is a parsed string, divided into * text-rendering components. Each component, being a * call to the Renderer. For instance, if you by tags * change the color, then the GUI will have to make * individual calls saying it want that color on the * text. * * For instance: * "Hello [b]there[/b] bunny!" * * That without word-wrapping would mean 3 components. * i.e. 3 calls to CRenderer. One drawing "Hello", * one drawing "there" in bold, and one drawing "bunny!". */ class CGUIText { public: /** * A sprite call to the CRenderer */ struct SSpriteCall { // The CGUISpriteInstance makes this uncopyable to avoid invalidating its draw cache NONCOPYABLE(SSpriteCall); MOVABLE(SSpriteCall); SSpriteCall() {} /** * Size and position of sprite */ CRect m_Area; /** * Sprite from global GUI sprite database. */ CGUISpriteInstance m_Sprite; }; /** * A text call to the CRenderer */ struct STextCall { NONCOPYABLE(STextCall); MOVABLE(STextCall); STextCall() : m_UseCustomColor(false), m_Bold(false), m_Italic(false), m_Underlined(false), m_pSpriteCall(nullptr) {} /** * Position */ CVector2D m_Pos; /** * Size */ CSize2D m_Size; /** * The string that is suppose to be rendered. */ CStrW m_String; /** * Use custom color? If true then m_Color is used, * else the color inputted will be used. */ bool m_UseCustomColor; /** * Color setup */ CGUIColor m_Color; /** * Font name */ CStrIntern m_Font; /** * Settings */ bool m_Bold, m_Italic, m_Underlined; /** * Tooltip text */ CStrW m_Tooltip; /** * *IF* an icon, then this is not nullptr. */ std::list::pointer m_pSpriteCall; }; // The SSpriteCall CGUISpriteInstance makes this uncopyable to avoid invalidating its draw cache. // Also take advantage of exchanging the containers directly with move semantics. NONCOPYABLE(CGUIText); MOVABLE(CGUIText); /** * Generates empty text. */ CGUIText() = default; /** * Generate a CGUIText object from the inputted string. * The function will break down the string and its * tags to calculate exactly which rendering queries * will be sent to the Renderer. Also, horizontal alignment * is taken into acount in this method but NOT vertical alignment. * * @param Text Text to generate CGUIText object from * @param Font Default font, notice both Default color and default font * can be changed by tags. * @param Width Width, 0 if no word-wrapping. * @param BufferZone space between text and edge, and space between text and images. * @param Align Horizontal alignment (left / center / right). * @param pObject Optional parameter for error output. Used *only* if error parsing fails, * and we need to be able to output which object the error occurred in to aid the user. */ CGUIText(const CGUI& pGUI, const CGUIString& string, const CStrW& FontW, const float Width, const float BufferZone, const EAlign align, const IGUIObject* pObject); /** * Draw this CGUIText object */ - void Draw(CGUI& pGUI, const CGUIColor& DefaultColor, const CVector2D& pos, const CRect& clipping) const; + void Draw(CGUI& pGUI, const CGUIColor& DefaultColor, const CVector2D& pos, CRect clipping) const; const CSize2D& GetSize() const { return m_Size; } const std::list& GetSpriteCalls() const { return m_SpriteCalls; } const std::vector& GetTextCalls() const { return m_TextCalls; } // Helper functions of the constructor bool ProcessLine( const CGUI& pGUI, const CGUIString& string, const CStrIntern& Font, const IGUIObject* pObject, const SGenerateTextImages& Images, const EAlign align, const float prelim_line_height, const float Width, const float BufferZone, bool& FirstLine, float& x, float& y, int& i, int& from); void SetupSpriteCalls( const CGUI& pGUI, const std::array, 2>& FeedbackImages, const float y, const float Width, const float BufferZone, const int i, const int pos_last_img, SGenerateTextImages& Images); float GetLineOffset( const EAlign align, const float width_range_from, const float width_range_to, const CSize2D& line_size) const; void ComputeLineRange( const SGenerateTextImages& Images, const float y, const float Width, const float prelim_line_height, float& width_range_from, float& width_range_to) const; void ComputeLineSize( const CGUI& pGUI, const CGUIString& string, const CStrIntern& Font, const bool FirstLine, const float Width, const float width_range_to, const int i, const int temp_from, float& x, CSize2D& line_size) const; bool AssembleCalls( const CGUI& pGUI, const CGUIString& string, const CStrIntern& Font, const IGUIObject* pObject, const bool FirstLine, const float Width, const float width_range_to, const float dx, const float y, const int temp_from, const int i, float& x, int& from); /** * List of TextCalls, for instance "Hello", "there!" */ std::vector m_TextCalls; /** * List of sprites, or "icons" that should be rendered * along with the text. */ std::list m_SpriteCalls; // list for consistent mem addresses // so that we can point to elements. /** * Width and height of the whole output, used when setting up * scrollbars and such. */ CSize2D m_Size; }; struct SGenerateTextImage { // The image's starting location in Y float m_YFrom; // The image's end location in Y float m_YTo; // The image width in other words float m_Indentation; void SetupSpriteCall( const bool Left, CGUIText::SSpriteCall& SpriteCall, const float width, const float y, const CSize2D& Size, const CStr& TextureName, const float BufferZone); }; #endif // INCLUDED_GUITEXT