Index: ps/trunk/source/gui/CGUIText.cpp =================================================================== --- ps/trunk/source/gui/CGUIText.cpp (revision 26869) +++ ps/trunk/source/gui/CGUIText.cpp (revision 26870) @@ -1,465 +1,468 @@ /* Copyright (C) 2022 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/Canvas2D.h" #include "graphics/FontMetrics.h" #include "graphics/TextRenderer.h" #include "gui/CGUI.h" #include "gui/ObjectBases/IGUIObject.h" #include "gui/SettingTypes/CGUIString.h" #include "ps/CStrInternStatic.h" #include "ps/VideoMode.h" #include "renderer/backend/IDeviceCommandContext.h" #include "renderer/Renderer.h" #include extern int g_xres, g_yres; // 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 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 posLastImage = -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 prelimLineHeight = 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, posLastImage, images); posLastImage = std::max(posLastImage, i); prelimLineHeight = std::max(prelimLineHeight, feedback.m_Size.Height); // If width is 0, then there's no word-wrapping, disable NewLine. if (((width != 0 && (feedback.m_Size.Width + 2 * bufferZone > width || feedback.m_NewLine)) || i == static_cast(string.m_Words.size()) - 2) && ProcessLine(pGUI, string, font, pObject, images, align, prelimLineHeight, width, bufferZone, firstLine, 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 posLastImage, SGenerateTextImages& images) { // Check if this has already been processed. // Also, floating images are only applicable if Word-Wrapping is on if (width == 0 || i <= posLastImage) 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 widthRangeFrom, const float widthRangeTo, const int i, const int tempFrom, CSize2D& lineSize) const { float x = widthRangeFrom; for (int j = tempFrom; 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 > widthRangeTo && j != tempFrom && !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); lineSize.Width -= currentFont.GetCharacterWidth(*L" "); break; } // Let lineSize.cy be the maximum m_Height we encounter. lineSize.Height = std::max(lineSize.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; lineSize.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 prelimLineHeight, const float width, const float bufferZone, bool& firstLine, float& y, int& i, int& from) { // Change 'from' to 'i', but first keep a copy of its value. int tempFrom = from; from = i; float widthRangeFrom = bufferZone; float widthRangeTo = width - bufferZone; ComputeLineRange(images, y, width, prelimLineHeight, widthRangeFrom, widthRangeTo); CSize2D lineSize; ComputeLineSize(pGUI, string, font, firstLine, width, widthRangeFrom, widthRangeTo, i, tempFrom, lineSize); // Move down, because font drawing starts from the baseline y += lineSize.Height; // Do the real processing now const bool done = AssembleCalls(pGUI, string, font, pObject, firstLine, width, widthRangeTo, GetLineOffset(align, widthRangeFrom, widthRangeTo, lineSize), y, tempFrom, i, from); // Update dimensions m_Size.Width = std::max(m_Size.Width, lineSize.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 lineSize.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 prelimLineHeight, float& widthRangeFrom, float& widthRangeTo) 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 unionFrom, unionTo; unionFrom = std::max(y, img.m_YFrom); unionTo = std::min(y + prelimLineHeight, img.m_YTo); // The union is not empty if (unionTo > unionFrom) { if (j == 0) widthRangeFrom = std::max(widthRangeFrom, img.m_Indentation); else widthRangeTo = std::min(widthRangeTo, width - img.m_Indentation); } } } // compute offset based on what kind of alignment float CGUIText::GetLineOffset( const EAlign align, const float widthRangeFrom, const float widthRangeTo, const CSize2D& lineSize) const { switch (align) { case EAlign::LEFT: return widthRangeFrom; case EAlign::CENTER: return (widthRangeTo + widthRangeFrom - lineSize.Width) / 2; case EAlign::RIGHT: return widthRangeTo - lineSize.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 widthRangeTo, const float dx, const float y, const int tempFrom, const int i, int& from) { bool done = false; float x = 0.f; for (int j = tempFrom; 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 xPointer is for. float xPointer = 0.f; for (STextCall& tc : feedback2.m_TextCalls) { tc.m_Pos = CVector2D(dx + x + xPointer, y); xPointer += 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 > widthRangeTo && j == tempFrom) { from = j+1; // do not break, since we want it to be added to m_TextCalls } else if (x > widthRangeTo) { 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, CCanvas2D& canvas, const CGUIColor& DefaultColor, const CVector2D& pos, CRect clipping) const { Renderer::Backend::IDeviceCommandContext* deviceCommandContext = g_Renderer.GetDeviceCommandContext(); - bool isClipped = clipping != CRect(); + const 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); + if (clipping.GetWidth() <= 0.0f || clipping.GetHeight() <= 0.0f) + return; + const float scale = g_VideoMode.GetScale(); Renderer::Backend::IDeviceCommandContext::Rect scissorRect; scissorRect.x = std::ceil(clipping.left * scale); scissorRect.y = std::ceil(g_yres - clipping.bottom * scale); scissorRect.width = std::floor(clipping.GetWidth() * scale); scissorRect.height = std::floor(clipping.GetHeight() * scale); // TODO: move scissors to CCanvas2D. deviceCommandContext->SetScissors(1, &scissorRect); } CTextRenderer textRenderer; textRenderer.SetClippingRect(clipping); textRenderer.Translate(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.SetCurrentColor(tc.m_UseCustomColor ? tc.m_Color : DefaultColor); textRenderer.SetCurrentFont(tc.m_Font); textRenderer.Put(floorf(pos.X + tc.m_Pos.X), floorf(pos.Y + tc.m_Pos.Y), &tc.m_String); } canvas.DrawText(textRenderer); for (const SSpriteCall& sc : m_SpriteCalls) pGUI.DrawSprite(sc.m_Sprite, canvas, sc.m_Area + pos); if (isClipped) deviceCommandContext->SetScissors(0, nullptr); } Index: ps/trunk/source/gui/ObjectTypes/CInput.cpp =================================================================== --- ps/trunk/source/gui/ObjectTypes/CInput.cpp (revision 26869) +++ ps/trunk/source/gui/ObjectTypes/CInput.cpp (revision 26870) @@ -1,2115 +1,2121 @@ /* Copyright (C) 2022 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 "CInput.h" #include "graphics/Canvas2D.h" #include "graphics/FontMetrics.h" #include "graphics/TextRenderer.h" #include "gui/CGUI.h" #include "gui/CGUIScrollBarVertical.h" #include "lib/timer.h" #include "lib/utf8.h" #include "ps/ConfigDB.h" #include "ps/CStrInternStatic.h" #include "ps/GameSetup/Config.h" #include "ps/Globals.h" #include "ps/Hotkey.h" #include "ps/VideoMode.h" #include "renderer/backend/IDeviceCommandContext.h" #include "renderer/Renderer.h" #include extern int g_yres; const CStr CInput::EventNameTextEdit = "TextEdit"; const CStr CInput::EventNamePress = "Press"; const CStr CInput::EventNameTab = "Tab"; CInput::CInput(CGUI& pGUI) : IGUIObject(pGUI), IGUIScrollBarOwner(*static_cast(this)), m_iBufferPos(-1), m_iBufferPos_Tail(-1), m_SelectingText(), m_HorizontalScroll(), m_PrevTime(), m_CursorVisState(true), m_CursorBlinkRate(0.5), m_ComposingText(), m_GeneratedPlaceholderTextValid(false), m_iComposedLength(), m_iComposedPos(), m_iInsertPos(), m_BufferPosition(this, "buffer_position"), m_BufferZone(this, "buffer_zone"), m_Caption(this, "caption"), m_Font(this, "font"), m_MaskChar(this, "mask_char"), m_Mask(this, "mask"), m_MaxLength(this, "max_length"), m_MultiLine(this, "multiline"), m_Readonly(this, "readonly"), m_ScrollBar(this, "scrollbar"), m_ScrollBarStyle(this, "scrollbar_style"), m_Sprite(this, "sprite"), m_SpriteOverlay(this, "sprite_overlay"), m_SpriteSelectArea(this, "sprite_selectarea"), m_TextColor(this, "textcolor"), m_TextColorSelected(this, "textcolor_selected"), m_PlaceholderText(this, "placeholder_text"), m_PlaceholderColor(this, "placeholder_color") { CFG_GET_VAL("gui.cursorblinkrate", m_CursorBlinkRate); auto bar = std::make_unique(pGUI); bar->SetRightAligned(true); AddScrollBar(std::move(bar)); } CInput::~CInput() { } void CInput::UpdateBufferPositionSetting() { m_BufferPosition.Set(m_iBufferPos, false); } void CInput::ClearComposedText() { m_Caption.GetMutable().erase(m_iInsertPos, m_iComposedLength); m_iBufferPos = m_iInsertPos; UpdateBufferPositionSetting(); m_iComposedLength = 0; m_iComposedPos = 0; } InReaction CInput::ManuallyHandleKeys(const SDL_Event_* ev) { ENSURE(m_iBufferPos != -1); // Get direct access to silently mutate m_Caption. // (Messages don't currently need to be sent) CStrW& caption = m_Caption.GetMutable(); switch (ev->ev.type) { case SDL_HOTKEYDOWN: { if (m_ComposingText) return IN_HANDLED; return ManuallyHandleHotkeyEvent(ev); } // SDL2 has a new method of text input that better supports Unicode and CJK // see https://wiki.libsdl.org/Tutorials/TextInput case SDL_TEXTINPUT: { if (m_Readonly) return IN_PASS; // Text has been committed, either single key presses or through an IME std::wstring text = wstring_from_utf8(ev->ev.text.text); // Check max length if (m_MaxLength != 0 && caption.length() + text.length() > static_cast(m_MaxLength)) return IN_HANDLED; m_WantedX = 0.0f; if (SelectingText()) DeleteCurSelection(); if (m_ComposingText) { ClearComposedText(); m_ComposingText = false; } if (m_iBufferPos == static_cast(caption.length())) caption.append(text); else caption.insert(m_iBufferPos, text); UpdateText(m_iBufferPos, m_iBufferPos, m_iBufferPos+1); m_iBufferPos += text.length(); UpdateBufferPositionSetting(); m_iBufferPos_Tail = -1; UpdateAutoScroll(); SendEvent(GUIM_TEXTEDIT, EventNameTextEdit); return IN_HANDLED; } case SDL_TEXTEDITING: { if (m_Readonly) return IN_PASS; // Text is being composed with an IME // TODO: indicate this by e.g. underlining the uncommitted text const char* rawText = ev->ev.edit.text; int rawLength = strlen(rawText); std::wstring wtext = wstring_from_utf8(rawText); m_WantedX = 0.0f; if (SelectingText()) DeleteCurSelection(); // Remember cursor position when text composition begins if (!m_ComposingText) m_iInsertPos = m_iBufferPos; else { // Composed text is replaced each time ClearComposedText(); } m_ComposingText = ev->ev.edit.start != 0 || rawLength != 0; if (m_ComposingText) { caption.insert(m_iInsertPos, wtext); // The text buffer is limited to SDL_TEXTEDITINGEVENT_TEXT_SIZE bytes, yet start // increases without limit, so don't let it advance beyond the composed text length m_iComposedLength = wtext.length(); m_iComposedPos = ev->ev.edit.start < m_iComposedLength ? ev->ev.edit.start : m_iComposedLength; m_iBufferPos = m_iInsertPos + m_iComposedPos; // TODO: composed text selection - what does ev.edit.length do? m_iBufferPos_Tail = -1; } UpdateBufferPositionSetting(); UpdateText(m_iBufferPos, m_iBufferPos, m_iBufferPos+1); UpdateAutoScroll(); SendEvent(GUIM_TEXTEDIT, EventNameTextEdit); return IN_HANDLED; } case SDL_KEYDOWN: case SDL_KEYUP: { // Since the GUI framework doesn't handle to set settings // in Unicode (CStrW), we'll simply retrieve the actual // pointer and edit that. SDL_Keycode keyCode = ev->ev.key.keysym.sym; // We have a probably printable key - we should return HANDLED so it can't trigger hotkeys. // However, if Ctrl/Meta modifiers are active, just pass it through instead, // assuming that we are indeed trying to trigger hotkeys (e.g. copy/paste). // Escape & the "cancel" hotkey are also passed through to allow closing dialogs easily. // See also similar logic in CConsole.cpp // NB: this assumes that Ctrl/GUI aren't used in the Manually* functions below, // as those code paths would obviously never be taken. if (keyCode == SDLK_ESCAPE || EventWillFireHotkey(ev, "cancel") || g_scancodes[SDL_SCANCODE_LCTRL] || g_scancodes[SDL_SCANCODE_RCTRL] || g_scancodes[SDL_SCANCODE_LGUI] || g_scancodes[SDL_SCANCODE_RGUI]) return IN_PASS; if (m_ComposingText) return IN_HANDLED; if (ev->ev.type == SDL_KEYDOWN) { ManuallyImmutableHandleKeyDownEvent(keyCode); ManuallyMutableHandleKeyDownEvent(keyCode); UpdateBufferPositionSetting(); } return IN_HANDLED; } default: { return IN_PASS; } } } void CInput::ManuallyMutableHandleKeyDownEvent(const SDL_Keycode keyCode) { if (m_Readonly) return; wchar_t cooked = 0; // Get direct access to silently mutate m_Caption. // (Messages don't currently need to be sent) CStrW& caption = m_Caption.GetMutable(); switch (keyCode) { case SDLK_TAB: { SendEvent(GUIM_TAB, EventNameTab); // Don't send a textedit event, because it should only // be sent if the GUI control changes the text break; } case SDLK_BACKSPACE: { m_WantedX = 0.0f; if (SelectingText()) DeleteCurSelection(); else { m_iBufferPos_Tail = -1; if (caption.empty() || m_iBufferPos == 0) break; if (m_iBufferPos == static_cast(caption.length())) caption = caption.Left(static_cast(caption.length()) - 1); else caption = caption.Left(m_iBufferPos - 1) + caption.Right(static_cast(caption.length()) - m_iBufferPos); --m_iBufferPos; UpdateText(m_iBufferPos, m_iBufferPos + 1, m_iBufferPos); } UpdateAutoScroll(); SendEvent(GUIM_TEXTEDIT, EventNameTextEdit); break; } case SDLK_DELETE: { m_WantedX = 0.0f; if (SelectingText()) DeleteCurSelection(); else { if (caption.empty() || m_iBufferPos == static_cast(caption.length())) break; caption = caption.Left(m_iBufferPos) + caption.Right(static_cast(caption.length()) - (m_iBufferPos + 1)); UpdateText(m_iBufferPos, m_iBufferPos + 1, m_iBufferPos); } UpdateAutoScroll(); SendEvent(GUIM_TEXTEDIT, EventNameTextEdit); break; } case SDLK_KP_ENTER: case SDLK_RETURN: { // 'Return' should do a Press event for single liners (e.g. submitting forms) // otherwise a '\n' character will be added. if (!m_MultiLine) { SendEvent(GUIM_PRESSED, EventNamePress); break; } cooked = '\n'; // Change to '\n' and do default: FALLTHROUGH; } default: // Insert a character { // Regular input is handled via SDL_TEXTINPUT, so we should ignore it here. if (cooked == 0) return; // Check max length if (m_MaxLength != 0 && caption.length() >= static_cast(m_MaxLength)) break; m_WantedX = 0.0f; if (SelectingText()) DeleteCurSelection(); m_iBufferPos_Tail = -1; if (m_iBufferPos == static_cast(caption.length())) caption += cooked; else caption = caption.Left(m_iBufferPos) + cooked + caption.Right(static_cast(caption.length()) - m_iBufferPos); UpdateText(m_iBufferPos, m_iBufferPos, m_iBufferPos + 1); ++m_iBufferPos; UpdateAutoScroll(); SendEvent(GUIM_TEXTEDIT, EventNameTextEdit); break; } } } void CInput::ManuallyImmutableHandleKeyDownEvent(const SDL_Keycode keyCode) { bool shiftKeyPressed = g_scancodes[SDL_SCANCODE_LSHIFT] || g_scancodes[SDL_SCANCODE_RSHIFT]; const CStrW& caption = *m_Caption; switch (keyCode) { case SDLK_HOME: { // If there's not a selection, we should create one now if (!shiftKeyPressed) { // Make sure a selection isn't created. m_iBufferPos_Tail = -1; } else if (!SelectingText()) { // Place tail at the current point: m_iBufferPos_Tail = m_iBufferPos; } m_iBufferPos = 0; m_WantedX = 0.0f; UpdateAutoScroll(); break; } case SDLK_END: { // If there's not a selection, we should create one now if (!shiftKeyPressed) { // Make sure a selection isn't created. m_iBufferPos_Tail = -1; } else if (!SelectingText()) { // Place tail at the current point: m_iBufferPos_Tail = m_iBufferPos; } m_iBufferPos = static_cast(caption.length()); m_WantedX = 0.0f; UpdateAutoScroll(); break; } /** * Conventions for Left/Right when text is selected: * * References: * * Visual Studio * Visual Studio has the 'newer' approach, used by newer versions of * things, and in newer applications. A left press will always place * the pointer on the left edge of the selection, and then of course * remove the selection. Right will do the exact same thing. * If you have the pointer on the right edge and press right, it will * in other words just remove the selection. * * Windows (eg. Notepad) * A left press always takes the pointer a step to the left and * removes the selection as if it were never there in the first place. * Right of course does the same thing but to the right. * * I chose the Visual Studio convention. Used also in Word, gtk 2.0, MSN * Messenger. */ case SDLK_LEFT: { m_WantedX = 0.f; if (shiftKeyPressed || !SelectingText()) { if (!shiftKeyPressed) m_iBufferPos_Tail = -1; else if (!SelectingText()) m_iBufferPos_Tail = m_iBufferPos; if (m_iBufferPos > 0) --m_iBufferPos; } else { if (m_iBufferPos_Tail < m_iBufferPos) m_iBufferPos = m_iBufferPos_Tail; m_iBufferPos_Tail = -1; } UpdateAutoScroll(); break; } case SDLK_RIGHT: { m_WantedX = 0.0f; if (shiftKeyPressed || !SelectingText()) { if (!shiftKeyPressed) m_iBufferPos_Tail = -1; else if (!SelectingText()) m_iBufferPos_Tail = m_iBufferPos; if (m_iBufferPos < static_cast(caption.length())) ++m_iBufferPos; } else { if (m_iBufferPos_Tail > m_iBufferPos) m_iBufferPos = m_iBufferPos_Tail; m_iBufferPos_Tail = -1; } UpdateAutoScroll(); break; } /** * Conventions for Up/Down when text is selected: * * References: * * Visual Studio * Visual Studio has a very strange approach, down takes you below the * selection to the next row, and up to the one prior to the whole * selection. The weird part is that it is always aligned as the * 'pointer'. I decided this is to much work for something that is * a bit arbitrary * * Windows (eg. Notepad) * Just like with left/right, the selection is destroyed and it moves * just as if there never were a selection. * * I chose the Notepad convention even though I use the VS convention with * left/right. */ case SDLK_UP: { if (!shiftKeyPressed) m_iBufferPos_Tail = -1; else if (!SelectingText()) m_iBufferPos_Tail = m_iBufferPos; std::list::iterator current = m_CharacterPositions.begin(); while (current != m_CharacterPositions.end()) { if (m_iBufferPos >= current->m_ListStart && m_iBufferPos <= current->m_ListStart + (int)current->m_ListOfX.size()) break; ++current; } float pos_x; if (m_iBufferPos - current->m_ListStart == 0) pos_x = 0.f; else pos_x = current->m_ListOfX[m_iBufferPos - current->m_ListStart - 1]; if (m_WantedX > pos_x) pos_x = m_WantedX; // Now change row: if (current != m_CharacterPositions.begin()) { --current; // Find X-position: m_iBufferPos = current->m_ListStart + GetXTextPosition(current, pos_x, m_WantedX); } // else we can't move up UpdateAutoScroll(); break; } case SDLK_DOWN: { if (!shiftKeyPressed) m_iBufferPos_Tail = -1; else if (!SelectingText()) m_iBufferPos_Tail = m_iBufferPos; std::list::iterator current = m_CharacterPositions.begin(); while (current != m_CharacterPositions.end()) { if (m_iBufferPos >= current->m_ListStart && m_iBufferPos <= current->m_ListStart + (int)current->m_ListOfX.size()) break; ++current; } float pos_x; if (m_iBufferPos - current->m_ListStart == 0) pos_x = 0.f; else pos_x = current->m_ListOfX[m_iBufferPos - current->m_ListStart - 1]; if (m_WantedX > pos_x) pos_x = m_WantedX; // Now change row: // Add first, so we can check if it's .end() ++current; if (current != m_CharacterPositions.end()) { // Find X-position: m_iBufferPos = current->m_ListStart + GetXTextPosition(current, pos_x, m_WantedX); } // else we can't move up UpdateAutoScroll(); break; } case SDLK_PAGEUP: { GetScrollBar(0).ScrollMinusPlenty(); UpdateAutoScroll(); break; } case SDLK_PAGEDOWN: { GetScrollBar(0).ScrollPlusPlenty(); UpdateAutoScroll(); break; } default: { break; } } } void CInput::SetupGeneratedPlaceholderText() { m_GeneratedPlaceholderText = CGUIText(m_pGUI, m_PlaceholderText, m_Font, 0, m_BufferZone, EAlign::LEFT, this); m_GeneratedPlaceholderTextValid = true; } InReaction CInput::ManuallyHandleHotkeyEvent(const SDL_Event_* ev) { bool shiftKeyPressed = g_scancodes[SDL_SCANCODE_LSHIFT] || g_scancodes[SDL_SCANCODE_RSHIFT]; std::string hotkey = static_cast(ev->ev.user.data1); // Get direct access to silently mutate m_Caption. // (Messages don't currently need to be sent) CStrW& caption = m_Caption.GetMutable(); if (hotkey == "paste") { if (m_Readonly) return IN_PASS; m_WantedX = 0.0f; char* utf8_text = SDL_GetClipboardText(); if (!utf8_text) return IN_HANDLED; std::wstring text = wstring_from_utf8(utf8_text); SDL_free(utf8_text); // Check max length if (m_MaxLength != 0 && caption.length() + text.length() > static_cast(m_MaxLength)) text = text.substr(0, static_cast(m_MaxLength) - caption.length()); if (SelectingText()) DeleteCurSelection(); if (m_iBufferPos == static_cast(caption.length())) caption += text; else caption = caption.Left(m_iBufferPos) + text + caption.Right(static_cast(caption.length()) - m_iBufferPos); UpdateText(m_iBufferPos, m_iBufferPos, m_iBufferPos+1); m_iBufferPos += static_cast(text.size()); UpdateAutoScroll(); UpdateBufferPositionSetting(); SendEvent(GUIM_TEXTEDIT, EventNameTextEdit); return IN_HANDLED; } else if (hotkey == "copy" || hotkey == "cut") { if (m_Readonly && hotkey == "cut") return IN_PASS; m_WantedX = 0.0f; if (SelectingText()) { int virtualFrom; int virtualTo; if (m_iBufferPos_Tail >= m_iBufferPos) { virtualFrom = m_iBufferPos; virtualTo = m_iBufferPos_Tail; } else { virtualFrom = m_iBufferPos_Tail; virtualTo = m_iBufferPos; } CStrW text = caption.Left(virtualTo).Right(virtualTo - virtualFrom); SDL_SetClipboardText(text.ToUTF8().c_str()); if (hotkey == "cut") { DeleteCurSelection(); UpdateAutoScroll(); SendEvent(GUIM_TEXTEDIT, EventNameTextEdit); } } return IN_HANDLED; } else if (hotkey == "text.delete.left") { if (m_Readonly) return IN_PASS; m_WantedX = 0.0f; if (SelectingText()) DeleteCurSelection(); if (!caption.empty() && m_iBufferPos != 0) { m_iBufferPos_Tail = m_iBufferPos; CStrW searchString = caption.Left(m_iBufferPos); // If we are starting in whitespace, adjust position until we get a non whitespace while (m_iBufferPos > 0) { if (!iswspace(searchString[m_iBufferPos - 1])) break; m_iBufferPos--; } // If we end up on a punctuation char we just delete it (treat punct like a word) if (iswpunct(searchString[m_iBufferPos - 1])) m_iBufferPos--; else { // Now we are on a non white space character, adjust position to char after next whitespace char is found while (m_iBufferPos > 0) { if (iswspace(searchString[m_iBufferPos - 1]) || iswpunct(searchString[m_iBufferPos - 1])) break; m_iBufferPos--; } } UpdateBufferPositionSetting(); DeleteCurSelection(); SendEvent(GUIM_TEXTEDIT, EventNameTextEdit); } UpdateAutoScroll(); return IN_HANDLED; } else if (hotkey == "text.delete.right") { if (m_Readonly) return IN_PASS; m_WantedX = 0.0f; if (SelectingText()) DeleteCurSelection(); if (!caption.empty() && m_iBufferPos < static_cast(caption.length())) { // Delete the word to the right of the cursor m_iBufferPos_Tail = m_iBufferPos; // Delete chars to the right unit we hit whitespace while (++m_iBufferPos < static_cast(caption.length())) { if (iswspace(caption[m_iBufferPos]) || iswpunct(caption[m_iBufferPos])) break; } // Eliminate any whitespace behind the word we just deleted while (m_iBufferPos < static_cast(caption.length())) { if (!iswspace(caption[m_iBufferPos])) break; ++m_iBufferPos; } UpdateBufferPositionSetting(); DeleteCurSelection(); } UpdateAutoScroll(); SendEvent(GUIM_TEXTEDIT, EventNameTextEdit); return IN_HANDLED; } else if (hotkey == "text.move.left") { m_WantedX = 0.0f; if (shiftKeyPressed || !SelectingText()) { if (!shiftKeyPressed) m_iBufferPos_Tail = -1; else if (!SelectingText()) m_iBufferPos_Tail = m_iBufferPos; if (!caption.empty() && m_iBufferPos != 0) { CStrW searchString = caption.Left(m_iBufferPos); // If we are starting in whitespace, adjust position until we get a non whitespace while (m_iBufferPos > 0) { if (!iswspace(searchString[m_iBufferPos - 1])) break; m_iBufferPos--; } // If we end up on a puctuation char we just select it (treat punct like a word) if (iswpunct(searchString[m_iBufferPos - 1])) m_iBufferPos--; else { // Now we are on a non white space character, adjust position to char after next whitespace char is found while (m_iBufferPos > 0) { if (iswspace(searchString[m_iBufferPos - 1]) || iswpunct(searchString[m_iBufferPos - 1])) break; m_iBufferPos--; } } } } else { if (m_iBufferPos_Tail < m_iBufferPos) m_iBufferPos = m_iBufferPos_Tail; m_iBufferPos_Tail = -1; } UpdateBufferPositionSetting(); UpdateAutoScroll(); return IN_HANDLED; } else if (hotkey == "text.move.right") { m_WantedX = 0.0f; if (shiftKeyPressed || !SelectingText()) { if (!shiftKeyPressed) m_iBufferPos_Tail = -1; else if (!SelectingText()) m_iBufferPos_Tail = m_iBufferPos; if (!caption.empty() && m_iBufferPos < static_cast(caption.length())) { // Select chars to the right until we hit whitespace while (++m_iBufferPos < static_cast(caption.length())) { if (iswspace(caption[m_iBufferPos]) || iswpunct(caption[m_iBufferPos])) break; } // Also select any whitespace following the word we just selected while (m_iBufferPos < static_cast(caption.length())) { if (!iswspace(caption[m_iBufferPos])) break; ++m_iBufferPos; } } } else { if (m_iBufferPos_Tail > m_iBufferPos) m_iBufferPos = m_iBufferPos_Tail; m_iBufferPos_Tail = -1; } UpdateBufferPositionSetting(); UpdateAutoScroll(); return IN_HANDLED; } return IN_PASS; } void CInput::ResetStates() { IGUIObject::ResetStates(); IGUIScrollBarOwner::ResetStates(); } void CInput::HandleMessage(SGUIMessage& Message) { IGUIObject::HandleMessage(Message); IGUIScrollBarOwner::HandleMessage(Message); // Cleans up operator[] usage. const CStrW& caption = *m_Caption; switch (Message.type) { case GUIM_SETTINGS_UPDATED: { // Update scroll-bar // TODO Gee: (2004-09-01) Is this really updated each time it should? if (m_ScrollBar && (Message.value == "size" || Message.value == "z" || Message.value == "absolute")) { GetScrollBar(0).SetX(m_CachedActualSize.right); GetScrollBar(0).SetY(m_CachedActualSize.top); GetScrollBar(0).SetZ(GetBufferedZ()); GetScrollBar(0).SetLength(m_CachedActualSize.bottom - m_CachedActualSize.top); } // Update scrollbar if (Message.value == "scrollbar_style") GetScrollBar(0).SetScrollBarStyle(m_ScrollBarStyle); if (Message.value == "buffer_position") { m_iBufferPos = m_MaxLength != 0 ? std::min(m_MaxLength, m_BufferPosition) : m_BufferPosition; m_iBufferPos_Tail = -1; // position change resets selection } if (Message.value == "size" || Message.value == "z" || Message.value == "font" || Message.value == "absolute" || Message.value == "caption" || Message.value == "scrollbar" || Message.value == "scrollbar_style") { UpdateText(); } if (Message.value == "multiline") { if (!m_MultiLine) GetScrollBar(0).SetLength(0.f); else GetScrollBar(0).SetLength(m_CachedActualSize.bottom - m_CachedActualSize.top); UpdateText(); } if (Message.value == "placeholder_text" || Message.value == "size" || Message.value == "font" || Message.value == "z" || Message.value == "text_valign") { m_GeneratedPlaceholderTextValid = false; } UpdateAutoScroll(); break; } case GUIM_MOUSE_PRESS_LEFT: { // Check if we're selecting the scrollbar if (m_ScrollBar && m_MultiLine && GetScrollBar(0).GetStyle()) { if (m_pGUI.GetMousePos().X > m_CachedActualSize.right - GetScrollBar(0).GetStyle()->m_Width) break; } if (m_ComposingText) break; // Okay, this section is about pressing the mouse and // choosing where the point should be placed. For // instance, if we press between a and b, the point // should of course be placed accordingly. Other // special cases are handled like the input box norms. if (g_scancodes[SDL_SCANCODE_LSHIFT] || g_scancodes[SDL_SCANCODE_RSHIFT]) m_iBufferPos = GetMouseHoveringTextPosition(); else m_iBufferPos = m_iBufferPos_Tail = GetMouseHoveringTextPosition(); m_SelectingText = true; UpdateAutoScroll(); // If we immediately release the button it will just be seen as a click // for the user though. break; } case GUIM_MOUSE_DBLCLICK_LEFT: { if (m_ComposingText) break; if (caption.empty()) break; m_iBufferPos = m_iBufferPos_Tail = GetMouseHoveringTextPosition(); if (m_iBufferPos >= (int)caption.length()) m_iBufferPos = m_iBufferPos_Tail = caption.length() - 1; // See if we are clicking over whitespace if (iswspace(caption[m_iBufferPos])) { // see if we are in a section of whitespace greater than one character if ((m_iBufferPos + 1 < (int) caption.length() && iswspace(caption[m_iBufferPos + 1])) || (m_iBufferPos - 1 > 0 && iswspace(caption[m_iBufferPos - 1]))) { // // We are clicking in an area with more than one whitespace character // so we select both the word to the left and then the word to the right // // [1] First the left // skip the whitespace while (m_iBufferPos > 0) { if (!iswspace(caption[m_iBufferPos - 1])) break; m_iBufferPos--; } // now go until we hit white space or punctuation while (m_iBufferPos > 0) { if (iswspace(caption[m_iBufferPos - 1])) break; m_iBufferPos--; if (iswpunct(caption[m_iBufferPos])) break; } // [2] Then the right // go right until we are not in whitespace while (++m_iBufferPos_Tail < static_cast(caption.length())) { if (!iswspace(caption[m_iBufferPos_Tail])) break; } if (m_iBufferPos_Tail == static_cast(caption.length())) break; // now go to the right until we hit whitespace or punctuation while (++m_iBufferPos_Tail < static_cast(caption.length())) { if (iswspace(caption[m_iBufferPos_Tail]) || iswpunct(caption[m_iBufferPos_Tail])) break; } } else { // single whitespace so select word to the right while (++m_iBufferPos_Tail < static_cast(caption.length())) { if (!iswspace(caption[m_iBufferPos_Tail])) break; } if (m_iBufferPos_Tail == static_cast(caption.length())) break; // Don't include the leading whitespace m_iBufferPos = m_iBufferPos_Tail; // now go to the right until we hit whitespace or punctuation while (++m_iBufferPos_Tail < static_cast(caption.length())) { if (iswspace(caption[m_iBufferPos_Tail]) || iswpunct(caption[m_iBufferPos_Tail])) break; } } } else { // clicked on non-whitespace so select current word // go until we hit white space or punctuation while (m_iBufferPos > 0) { if (iswspace(caption[m_iBufferPos - 1])) break; m_iBufferPos--; if (iswpunct(caption[m_iBufferPos])) break; } // go to the right until we hit whitespace or punctuation while (++m_iBufferPos_Tail < static_cast(caption.length())) if (iswspace(caption[m_iBufferPos_Tail]) || iswpunct(caption[m_iBufferPos_Tail])) break; } UpdateAutoScroll(); break; } case GUIM_MOUSE_RELEASE_LEFT: { if (m_SelectingText) m_SelectingText = false; break; } case GUIM_MOUSE_MOTION: { // If we just pressed down and started to move before releasing // this is one way of selecting larger portions of text. if (m_SelectingText) { // Actually, first we need to re-check that the mouse button is // really pressed (it can be released while outside the control. if (!g_mouse_buttons[SDL_BUTTON_LEFT]) m_SelectingText = false; else m_iBufferPos = GetMouseHoveringTextPosition(); UpdateAutoScroll(); } break; } case GUIM_LOAD: { GetScrollBar(0).SetX(m_CachedActualSize.right); GetScrollBar(0).SetY(m_CachedActualSize.top); GetScrollBar(0).SetZ(GetBufferedZ()); GetScrollBar(0).SetLength(m_CachedActualSize.bottom - m_CachedActualSize.top); GetScrollBar(0).SetScrollBarStyle(m_ScrollBarStyle); UpdateText(); UpdateAutoScroll(); break; } case GUIM_GOT_FOCUS: { m_iBufferPos = 0; m_PrevTime = 0.0; m_CursorVisState = false; ResetActiveHotkeys(); // Tell the IME where to draw the candidate list SDL_Rect rect; rect.h = m_CachedActualSize.GetSize().Height; rect.w = m_CachedActualSize.GetSize().Width; rect.x = m_CachedActualSize.TopLeft().X; rect.y = m_CachedActualSize.TopLeft().Y; SDL_SetTextInputRect(&rect); SDL_StartTextInput(); break; } case GUIM_LOST_FOCUS: { if (m_ComposingText) { // Simulate a final text editing event to clear the composition SDL_Event_ evt; evt.ev.type = SDL_TEXTEDITING; evt.ev.edit.length = 0; evt.ev.edit.start = 0; evt.ev.edit.text[0] = 0; ManuallyHandleKeys(&evt); } SDL_StopTextInput(); m_iBufferPos = -1; m_iBufferPos_Tail = -1; break; } default: { break; } } UpdateBufferPositionSetting(); } void CInput::UpdateCachedSize() { // If an ancestor's size changed, this will let us intercept the change and // update our scrollbar positions IGUIObject::UpdateCachedSize(); if (m_ScrollBar) { GetScrollBar(0).SetX(m_CachedActualSize.right); GetScrollBar(0).SetY(m_CachedActualSize.top); GetScrollBar(0).SetZ(GetBufferedZ()); GetScrollBar(0).SetLength(m_CachedActualSize.bottom - m_CachedActualSize.top); } m_GeneratedPlaceholderTextValid = false; } -void CInput::Draw(CCanvas2D& canvas) +void CInput::DrawContent(CCanvas2D& canvas) { - Renderer::Backend::IDeviceCommandContext* deviceCommandContext = - g_Renderer.GetDeviceCommandContext(); - if (m_CursorBlinkRate > 0.0) { // check if the cursor visibility state needs to be changed double currTime = timer_Time(); if (currTime - m_PrevTime >= m_CursorBlinkRate) { m_CursorVisState = !m_CursorVisState; m_PrevTime = currTime; } } else // should always be visible m_CursorVisState = true; CStrIntern font_name(m_Font->ToUTF8()); wchar_t mask_char = L'*'; if (m_Mask && m_MaskChar->length() > 0) mask_char = (*m_MaskChar)[0]; m_pGUI.DrawSprite(m_Sprite, canvas, m_CachedActualSize); float scroll = 0.f; if (m_ScrollBar && m_MultiLine) scroll = GetScrollBar(0).GetPos(); CFontMetrics font(font_name); - // We'll have to setup clipping manually, since we're doing the rendering manually. - CRect cliparea(m_CachedActualSize); - - // First we'll figure out the clipping area, which is the cached actual size - // substracted by an optional scrollbar - if (m_ScrollBar) - { - scroll = GetScrollBar(0).GetPos(); - - // substract scrollbar from cliparea - if (cliparea.right > GetScrollBar(0).GetOuterRect().left && - cliparea.right <= GetScrollBar(0).GetOuterRect().right) - cliparea.right = GetScrollBar(0).GetOuterRect().left; - - if (cliparea.left >= GetScrollBar(0).GetOuterRect().left && - cliparea.left < GetScrollBar(0).GetOuterRect().right) - cliparea.left = GetScrollBar(0).GetOuterRect().right; - } - - if (cliparea != CRect()) - { - const float scale = g_VideoMode.GetScale(); - Renderer::Backend::IDeviceCommandContext::Rect scissorRect; - scissorRect.x = cliparea.left * scale; - scissorRect.y = g_yres - cliparea.bottom * scale; - scissorRect.width = cliparea.GetWidth() * scale; - scissorRect.height = cliparea.GetHeight() * scale; - // TODO: move scissors to CCanvas2D. - deviceCommandContext->SetScissors(1, &scissorRect); - } - // These are useful later. int VirtualFrom, VirtualTo; if (m_iBufferPos_Tail >= m_iBufferPos) { VirtualFrom = m_iBufferPos; VirtualTo = m_iBufferPos_Tail; } else { VirtualFrom = m_iBufferPos_Tail; VirtualTo = m_iBufferPos; } // Get the height of this font. float h = (float)font.GetHeight(); float ls = (float)font.GetLineSpacing(); CTextRenderer textRenderer; textRenderer.SetCurrentFont(font_name); textRenderer.Translate( (float)(int)(m_CachedActualSize.left) + m_BufferZone, - (float)(int)(m_CachedActualSize.top+h) + m_BufferZone); + (float)(int)(m_CachedActualSize.top + h) + m_BufferZone); // U+FE33: PRESENTATION FORM FOR VERTICAL LOW LINE // (sort of like a | which is aligned to the left of most characters) float buffered_y = -scroll + m_BufferZone; // When selecting larger areas, we need to draw a rectangle box // around it, and this is to keep track of where the box // started, because we need to follow the iteration until we // reach the end, before we can actually draw it. bool drawing_box = false; float box_x = 0.f; float x_pointer = 0.f; // If we have a selecting box (i.e. when you have selected letters, not just when // the pointer is between two letters) we need to process all letters once // before we do it the second time and render all the text. We can't do it // in the same loop because text will have been drawn, so it will disappear when // drawn behind the text that has already been drawn. Confusing, well it's necessary // (I think). if (SelectingText()) { // Now m_iBufferPos_Tail can be of both sides of m_iBufferPos, // just like you can select from right to left, as you can // left to right. Is there a difference? Yes, the pointer // be placed accordingly, so that if you select shift and // expand this selection, it will expand on appropriate side. // Anyway, since the drawing procedure needs "To" to be // greater than from, we need virtual values that might switch // place. int virtualFrom = 0; int virtualTo = 0; if (m_iBufferPos_Tail >= m_iBufferPos) { virtualFrom = m_iBufferPos; virtualTo = m_iBufferPos_Tail; } else { virtualFrom = m_iBufferPos_Tail; virtualTo = m_iBufferPos; } bool done = false; for (std::list::const_iterator it = m_CharacterPositions.begin(); - it != m_CharacterPositions.end(); - ++it, buffered_y += ls, x_pointer = 0.f) + it != m_CharacterPositions.end(); + ++it, buffered_y += ls, x_pointer = 0.f) { if (m_MultiLine && buffered_y > m_CachedActualSize.GetHeight()) break; // We might as well use 'i' here to iterate, because we need it // (often compared against ints, so don't make it size_t) - for (int i = 0; i < (int)it->m_ListOfX.size()+2; ++i) + for (int i = 0; i < (int)it->m_ListOfX.size() + 2; ++i) { if (it->m_ListStart + i == virtualFrom) { // we won't actually draw it now, because we don't // know the width of each glyph to that position. // we need to go along with the iteration, and // make a mark where the box started: drawing_box = true; // will turn false when finally rendered. // Get current x position box_x = x_pointer; } - const bool at_end = (i == (int)it->m_ListOfX.size()+1); + const bool at_end = (i == (int)it->m_ListOfX.size() + 1); if (drawing_box && (it->m_ListStart + i == virtualTo || at_end)) { // Depending on if it's just a row change, or if it's // the end of the select box, do slightly different things. if (at_end) { if (it->m_ListStart + i != virtualFrom) // and actually add a white space! yes, this is done in any common input x_pointer += font.GetCharacterWidth(L' '); } else { drawing_box = false; done = true; } CRect rect; // Set 'rect' depending on if it's a multiline control, or a one-line control if (m_MultiLine) { rect = CRect( m_CachedActualSize.left + box_x + m_BufferZone, m_CachedActualSize.top + buffered_y + (h - ls) / 2, m_CachedActualSize.left + x_pointer + m_BufferZone, m_CachedActualSize.top + buffered_y + (h + ls) / 2); if (rect.bottom < m_CachedActualSize.top) continue; if (rect.top < m_CachedActualSize.top) rect.top = m_CachedActualSize.top; if (rect.bottom > m_CachedActualSize.bottom) rect.bottom = m_CachedActualSize.bottom; } else // if one-line { rect = CRect( m_CachedActualSize.left + box_x + m_BufferZone - m_HorizontalScroll, m_CachedActualSize.top + buffered_y + (h - ls) / 2, m_CachedActualSize.left + x_pointer + m_BufferZone - m_HorizontalScroll, m_CachedActualSize.top + buffered_y + (h + ls) / 2); if (rect.left < m_CachedActualSize.left) rect.left = m_CachedActualSize.left; if (rect.right > m_CachedActualSize.right) rect.right = m_CachedActualSize.right; } m_pGUI.DrawSprite(m_SpriteSelectArea, canvas, rect); } if (i < (int)it->m_ListOfX.size()) { if (!m_Mask) x_pointer += font.GetCharacterWidth((*m_Caption)[it->m_ListStart + i]); else x_pointer += font.GetCharacterWidth(mask_char); } } if (done) break; // If we're about to draw a box, and all of a sudden changes // line, we need to draw that line's box, and then reset // the box drawing to the beginning of the new line. if (drawing_box) box_x = 0.f; } } // Reset some from previous run buffered_y = -scroll; // Setup initial color (then it might change and change back, when drawing selected area) textRenderer.SetCurrentColor(m_TextColor); bool using_selected_color = false; for (std::list::const_iterator it = m_CharacterPositions.begin(); - it != m_CharacterPositions.end(); - ++it, buffered_y += ls) + it != m_CharacterPositions.end(); + ++it, buffered_y += ls) { if (buffered_y + m_BufferZone >= -ls || !m_MultiLine) { if (m_MultiLine && buffered_y + m_BufferZone > m_CachedActualSize.GetHeight()) break; const CVector2D savedTranslate = textRenderer.GetTranslate(); // Text must always be drawn in integer values. So we have to convert scroll if (m_MultiLine) textRenderer.Translate(0.f, -(float)(int)scroll); else textRenderer.Translate(-(float)(int)m_HorizontalScroll, 0.f); // We might as well use 'i' here, because we need it // (often compared against ints, so don't make it size_t) - for (int i = 0; i < (int)it->m_ListOfX.size()+1; ++i) + for (int i = 0; i < (int)it->m_ListOfX.size() + 1; ++i) { if (!m_MultiLine && i < (int)it->m_ListOfX.size()) { if (it->m_ListOfX[i] - m_HorizontalScroll < -m_BufferZone) { // We still need to translate the OpenGL matrix if (i == 0) textRenderer.Translate(it->m_ListOfX[i], 0.f); else - textRenderer.Translate(it->m_ListOfX[i] - it->m_ListOfX[i-1], 0.f); + textRenderer.Translate(it->m_ListOfX[i] - it->m_ListOfX[i - 1], 0.f); continue; } } // End of selected area, change back color if (SelectingText() && it->m_ListStart + i == VirtualTo) { using_selected_color = false; textRenderer.SetCurrentColor(m_TextColor); } // selecting only one, then we need only to draw a cursor. if (i != (int)it->m_ListOfX.size() && it->m_ListStart + i == m_iBufferPos && m_CursorVisState && !m_Readonly) textRenderer.Put(0.0f, 0.0f, L"_"); // Drawing selected area if (SelectingText() && - it->m_ListStart + i >= VirtualFrom && - it->m_ListStart + i < VirtualTo && - !using_selected_color) + it->m_ListStart + i >= VirtualFrom && + it->m_ListStart + i < VirtualTo && + !using_selected_color) { using_selected_color = true; textRenderer.SetCurrentColor(m_TextColorSelected); } if (i != (int)it->m_ListOfX.size()) { if (!m_Mask) textRenderer.PrintfAdvance(L"%lc", (*m_Caption)[it->m_ListStart + i]); else textRenderer.PrintfAdvance(L"%lc", mask_char); } // check it's now outside a one-liner, then we'll break if (!m_MultiLine && i < (int)it->m_ListOfX.size() && - it->m_ListOfX[i] - m_HorizontalScroll > m_CachedActualSize.GetWidth() - m_BufferZone) + it->m_ListOfX[i] - m_HorizontalScroll > m_CachedActualSize.GetWidth() - m_BufferZone) break; } if (it->m_ListStart + (int)it->m_ListOfX.size() == m_iBufferPos) { textRenderer.SetCurrentColor(m_TextColor); if (m_CursorVisState && !m_Readonly) textRenderer.PutAdvance(L"_"); if (using_selected_color) textRenderer.SetCurrentColor(m_TextColorSelected); } textRenderer.ResetTranslate(savedTranslate); } textRenderer.Translate(0.f, ls); } canvas.DrawText(textRenderer); +} + +void CInput::Draw(CCanvas2D& canvas) +{ + Renderer::Backend::IDeviceCommandContext* deviceCommandContext = + g_Renderer.GetDeviceCommandContext(); + + // We'll have to setup clipping manually, since we're doing the rendering manually. + CRect cliparea(m_CachedActualSize); + + // First we'll figure out the clipping area, which is the cached actual size + // substracted by an optional scrollbar + if (m_ScrollBar) + { + // substract scrollbar from cliparea + if (cliparea.right > GetScrollBar(0).GetOuterRect().left && + cliparea.right <= GetScrollBar(0).GetOuterRect().right) + cliparea.right = GetScrollBar(0).GetOuterRect().left; + + if (cliparea.left >= GetScrollBar(0).GetOuterRect().left && + cliparea.left < GetScrollBar(0).GetOuterRect().right) + cliparea.left = GetScrollBar(0).GetOuterRect().right; + } + + const bool isClipped = cliparea != CRect(); + if (isClipped) + { + if (cliparea.GetWidth() <= 0.0f || cliparea.GetHeight() <= 0.0f) + return; + const float scale = g_VideoMode.GetScale(); + Renderer::Backend::IDeviceCommandContext::Rect scissorRect; + scissorRect.x = cliparea.left * scale; + scissorRect.y = g_yres - cliparea.bottom * scale; + scissorRect.width = cliparea.GetWidth() * scale; + scissorRect.height = cliparea.GetHeight() * scale; + // TODO: move scissors to CCanvas2D. + deviceCommandContext->SetScissors(1, &scissorRect); + } + + DrawContent(canvas); - if (cliparea != CRect()) + if (isClipped) deviceCommandContext->SetScissors(0, nullptr); if (m_Caption->empty() && !m_PlaceholderText->GetRawString().empty()) DrawPlaceholderText(canvas, cliparea); // Draw scrollbars on top of the content if (m_ScrollBar && m_MultiLine) IGUIScrollBarOwner::Draw(canvas); // Draw the overlays last m_pGUI.DrawSprite(m_SpriteOverlay, canvas, m_CachedActualSize); } void CInput::DrawPlaceholderText(CCanvas2D& canvas, const CRect& clipping) { if (!m_GeneratedPlaceholderTextValid) SetupGeneratedPlaceholderText(); m_GeneratedPlaceholderText.Draw(m_pGUI, canvas, m_PlaceholderColor, m_CachedActualSize.TopLeft(), clipping); } void CInput::UpdateText(int from, int to_before, int to_after) { CStrW& caption = m_Caption.GetMutable(); if (m_MaxLength != 0 && caption.length() > static_cast(m_MaxLength)) caption = caption.substr(0, m_MaxLength); CStrIntern font_name(m_Font->ToUTF8()); wchar_t mask_char = L'*'; if (m_Mask && m_MaskChar->length() > 0) mask_char = (*m_MaskChar)[0]; // Ensure positions are valid after caption changes m_iBufferPos = std::min(m_iBufferPos, static_cast(caption.size())); m_iBufferPos_Tail = std::min(m_iBufferPos_Tail, static_cast(caption.size())); UpdateBufferPositionSetting(); if (font_name.empty()) { // Destroy everything stored, there's no font, so there can be no data. m_CharacterPositions.clear(); return; } SRow row; row.m_ListStart = 0; int to = 0; // make sure it's initialized if (to_before == -1) to = static_cast(caption.length()); CFontMetrics font(font_name); std::list::iterator current_line; // Used to ... TODO int check_point_row_start = -1; int check_point_row_end = -1; // Reset if (from == 0 && to_before == -1) { m_CharacterPositions.clear(); current_line = m_CharacterPositions.begin(); } else { ENSURE(to_before != -1); std::list::iterator destroy_row_from; std::list::iterator destroy_row_to; // Used to check if the above has been set to anything, // previously a comparison like: // destroy_row_from == std::list::iterator() // ... was used, but it didn't work with GCC. bool destroy_row_from_used = false; bool destroy_row_to_used = false; // Iterate, and remove everything between 'from' and 'to_before' // actually remove the entire lines they are on, it'll all have // to be redone. And when going along, we'll delete a row at a time // when continuing to see how much more after 'to' we need to remake. int i = 0; for (std::list::iterator it = m_CharacterPositions.begin(); it != m_CharacterPositions.end(); ++it, ++i) { if (!destroy_row_from_used && it->m_ListStart > from) { // Destroy the previous line, and all to 'to_before' destroy_row_from = it; --destroy_row_from; destroy_row_from_used = true; // For the rare case that we might remove characters to a word // so that it suddenly fits on the previous row, // we need to by standards re-do the whole previous line too // (if one exists) if (destroy_row_from != m_CharacterPositions.begin()) --destroy_row_from; } if (!destroy_row_to_used && it->m_ListStart > to_before) { destroy_row_to = it; destroy_row_to_used = true; // If it isn't the last row, we'll add another row to delete, // just so we can see if the last restorted line is // identical to what it was before. If it isn't, then we'll // have to continue. // 'check_point_row_start' is where we store how the that // line looked. if (destroy_row_to != m_CharacterPositions.end()) { check_point_row_start = destroy_row_to->m_ListStart; check_point_row_end = check_point_row_start + (int)destroy_row_to->m_ListOfX.size(); if (destroy_row_to->m_ListOfX.empty()) ++check_point_row_end; } ++destroy_row_to; break; } } if (!destroy_row_from_used) { destroy_row_from = m_CharacterPositions.end(); --destroy_row_from; // As usual, let's destroy another row back if (destroy_row_from != m_CharacterPositions.begin()) --destroy_row_from; current_line = destroy_row_from; } if (!destroy_row_to_used) { destroy_row_to = m_CharacterPositions.end(); check_point_row_start = -1; } // set 'from' to the row we'll destroy from // and 'to' to the row we'll destroy to from = destroy_row_from->m_ListStart; if (destroy_row_to != m_CharacterPositions.end()) to = destroy_row_to->m_ListStart; // notice it will iterate [from, to), so it will never reach to. else to = static_cast(caption.length()); // Setup the first row row.m_ListStart = destroy_row_from->m_ListStart; std::list::iterator temp_it = destroy_row_to; --temp_it; current_line = m_CharacterPositions.erase(destroy_row_from, destroy_row_to); // If there has been a change in number of characters // we need to change all m_ListStart that comes after // the interval we just destroyed. We'll change all // values with the delta change of the string length. int delta = to_after - to_before; if (delta != 0) { for (std::list::iterator it = current_line; it != m_CharacterPositions.end(); ++it) it->m_ListStart += delta; // Update our check point too! check_point_row_start += delta; check_point_row_end += delta; if (to != static_cast(caption.length())) to += delta; } } int last_word_started = from; float x_pos = 0.f; //if (to_before != -1) // return; for (int i = from; i < to; ++i) { if (caption[i] == L'\n' && m_MultiLine) { if (i == to-1 && to != static_cast(caption.length())) break; // it will be added outside current_line = m_CharacterPositions.insert(current_line, row); ++current_line; // Setup the next row: row.m_ListOfX.clear(); row.m_ListStart = i+1; x_pos = 0.f; } else { if (caption[i] == L' '/* || TODO Gee (2004-10-13): the '-' disappears, fix. caption[i] == L'-'*/) last_word_started = i+1; if (!m_Mask) x_pos += font.GetCharacterWidth(caption[i]); else x_pos += font.GetCharacterWidth(mask_char); if (x_pos >= GetTextAreaWidth() && m_MultiLine) { // The following decides whether it will word-wrap a word, // or if it's only one word on the line, where it has to // break the word apart. if (last_word_started == row.m_ListStart) { last_word_started = i; row.m_ListOfX.resize(row.m_ListOfX.size() - (i-last_word_started)); //row.m_ListOfX.push_back(x_pos); //continue; } else { // regular word-wrap row.m_ListOfX.resize(row.m_ListOfX.size() - (i-last_word_started+1)); } // Now, create a new line: // notice: when we enter a newline, you can stand with the cursor // both before and after that character, being on different // rows. With automatic word-wrapping, that is not possible. Which // is intuitively correct. current_line = m_CharacterPositions.insert(current_line, row); ++current_line; // Setup the next row: row.m_ListOfX.clear(); row.m_ListStart = last_word_started; i = last_word_started-1; x_pos = 0.f; } else // Get width of this character: row.m_ListOfX.push_back(x_pos); } // Check if it's the last iteration, and we're not revising the whole string // because in that case, more word-wrapping might be needed. // also check if the current line isn't the end if (to_before != -1 && i == to-1 && current_line != m_CharacterPositions.end()) { // check all rows and see if any existing if (row.m_ListStart != check_point_row_start) { std::list::iterator destroy_row_from; std::list::iterator destroy_row_to; // Are used to check if the above has been set to anything, // previously a comparison like: // destroy_row_from == std::list::iterator() // was used, but it didn't work with GCC. bool destroy_row_from_used = false; bool destroy_row_to_used = false; // Iterate, and remove everything between 'from' and 'to_before' // actually remove the entire lines they are on, it'll all have // to be redone. And when going along, we'll delete a row at a time // when continuing to see how much more after 'to' we need to remake. for (std::list::iterator it = m_CharacterPositions.begin(); it != m_CharacterPositions.end(); ++it) { if (!destroy_row_from_used && it->m_ListStart > check_point_row_start) { // Destroy the previous line, and all to 'to_before' //if (i >= 2) // destroy_row_from = it-2; //else // destroy_row_from = it-1; destroy_row_from = it; destroy_row_from_used = true; //--destroy_row_from; } if (!destroy_row_to_used && it->m_ListStart > check_point_row_end) { destroy_row_to = it; destroy_row_to_used = true; // If it isn't the last row, we'll add another row to delete, // just so we can see if the last restorted line is // identical to what it was before. If it isn't, then we'll // have to continue. // 'check_point_row_start' is where we store how the that // line looked. if (destroy_row_to != m_CharacterPositions.end()) { check_point_row_start = destroy_row_to->m_ListStart; check_point_row_end = check_point_row_start + (int)destroy_row_to->m_ListOfX.size(); if (destroy_row_to->m_ListOfX.empty()) ++check_point_row_end; } else check_point_row_start = check_point_row_end = -1; ++destroy_row_to; break; } } if (!destroy_row_from_used) { destroy_row_from = m_CharacterPositions.end(); --destroy_row_from; current_line = destroy_row_from; } if (!destroy_row_to_used) { destroy_row_to = m_CharacterPositions.end(); check_point_row_start = check_point_row_end = -1; } if (destroy_row_to != m_CharacterPositions.end()) to = destroy_row_to->m_ListStart; // notice it will iterate [from, to[, so it will never reach to. else to = static_cast(caption.length()); // Set current line, new rows will be added before current_line, so // we'll choose the destroy_row_to, because it won't be deleted // in the coming erase. current_line = destroy_row_to; m_CharacterPositions.erase(destroy_row_from, destroy_row_to); } // else, the for loop will end naturally. } } // This is kind of special, when we renew a some lines, then the last // one will sometimes end with a space (' '), that really should // be omitted when word-wrapping. So we'll check if the last row // we'll add has got the same value as the next row. if (current_line != m_CharacterPositions.end()) { if (row.m_ListStart + (int)row.m_ListOfX.size() == current_line->m_ListStart) row.m_ListOfX.resize(row.m_ListOfX.size()-1); } // add the final row (even if empty) m_CharacterPositions.insert(current_line, row); if (m_ScrollBar) { GetScrollBar(0).SetScrollRange(m_CharacterPositions.size() * font.GetLineSpacing() + m_BufferZone * 2.f); GetScrollBar(0).SetScrollSpace(m_CachedActualSize.GetHeight()); } } int CInput::GetMouseHoveringTextPosition() const { if (m_CharacterPositions.empty()) return 0; // Return position int retPosition; std::list::const_iterator current = m_CharacterPositions.begin(); CVector2D mouse = m_pGUI.GetMousePos(); if (m_MultiLine) { float scroll = 0.f; if (m_ScrollBar) scroll = GetScrollBarPos(0); // Now get the height of the font. // TODO: Get the real font CFontMetrics font(CStrIntern(m_Font->ToUTF8())); float spacing = (float)font.GetLineSpacing(); // Change mouse position relative to text. mouse -= m_CachedActualSize.TopLeft(); mouse.X -= m_BufferZone; mouse.Y += scroll - m_BufferZone; int row = (int)((mouse.Y) / spacing); if (row < 0) row = 0; if (row > (int)m_CharacterPositions.size()-1) row = (int)m_CharacterPositions.size()-1; // TODO Gee (2004-11-21): Okay, I need a 'std::list' for some reasons, but I would really like to // be able to get the specific element here. This is hopefully a temporary hack. for (int i = 0; i < row; ++i) ++current; } else { // current is already set to begin, // but we'll change the mouse.x to fit our horizontal scrolling mouse -= m_CachedActualSize.TopLeft(); mouse.X -= m_BufferZone - m_HorizontalScroll; // mouse.y is moot } retPosition = current->m_ListStart; // Okay, now loop through the glyphs to find the appropriate X position float dummy; retPosition += GetXTextPosition(current, mouse.X, dummy); return retPosition; } // Does not process horizontal scrolling, 'x' must be modified before inputted. int CInput::GetXTextPosition(const std::list::const_iterator& current, const float& x, float& wanted) const { int ret = 0; float previous = 0.f; int i = 0; for (std::vector::const_iterator it = current->m_ListOfX.begin(); it != current->m_ListOfX.end(); ++it, ++i) { if (*it >= x) { if (x - previous >= *it - x) ret += i+1; else ret += i; break; } previous = *it; } // If a position wasn't found, we will assume the last // character of that line. if (i == (int)current->m_ListOfX.size()) { ret += i; wanted = x; } else wanted = 0.f; return ret; } void CInput::DeleteCurSelection() { int virtualFrom; int virtualTo; if (m_iBufferPos_Tail >= m_iBufferPos) { virtualFrom = m_iBufferPos; virtualTo = m_iBufferPos_Tail; } else { virtualFrom = m_iBufferPos_Tail; virtualTo = m_iBufferPos; } // Silently change. m_Caption.Set(m_Caption->Left(virtualFrom) + m_Caption->Right(static_cast(m_Caption->length()) - virtualTo), false); UpdateText(virtualFrom, virtualTo, virtualFrom); // Remove selection m_iBufferPos_Tail = -1; m_iBufferPos = virtualFrom; UpdateBufferPositionSetting(); } bool CInput::SelectingText() const { return m_iBufferPos_Tail != -1 && m_iBufferPos_Tail != m_iBufferPos; } float CInput::GetTextAreaWidth() { if (m_ScrollBar && GetScrollBar(0).GetStyle()) return m_CachedActualSize.GetWidth() - m_BufferZone * 2.f - GetScrollBar(0).GetStyle()->m_Width; return m_CachedActualSize.GetWidth() - m_BufferZone * 2.f; } void CInput::UpdateAutoScroll() { // Autoscrolling up and down if (m_MultiLine) { if (!m_ScrollBar) return; const float scroll = GetScrollBar(0).GetPos(); // Now get the height of the font. // TODO: Get the real font CFontMetrics font(CStrIntern(m_Font->ToUTF8())); float spacing = (float)font.GetLineSpacing(); //float height = font.GetHeight(); // TODO Gee (2004-11-21): Okay, I need a 'std::list' for some reasons, but I would really like to // be able to get the specific element here. This is hopefully a temporary hack. std::list::iterator current = m_CharacterPositions.begin(); int row = 0; while (current != m_CharacterPositions.end()) { if (m_iBufferPos >= current->m_ListStart && m_iBufferPos <= current->m_ListStart + (int)current->m_ListOfX.size()) break; ++current; ++row; } // If scrolling down if (-scroll + static_cast(row + 1) * spacing + m_BufferZone * 2.f > m_CachedActualSize.GetHeight()) { // Scroll so the selected row is shown completely, also with m_BufferZone length to the edge. GetScrollBar(0).SetPos(static_cast(row + 1) * spacing - m_CachedActualSize.GetHeight() + m_BufferZone * 2.f); } // If scrolling up else if (-scroll + (float)row * spacing < 0.f) { // Scroll so the selected row is shown completely, also with m_BufferZone length to the edge. GetScrollBar(0).SetPos((float)row * spacing); } } else // autoscrolling left and right { // Get X position of position: if (m_CharacterPositions.empty()) return; float x_position = 0.f; float x_total = 0.f; if (!m_CharacterPositions.begin()->m_ListOfX.empty()) { // Get position of m_iBufferPos if ((int)m_CharacterPositions.begin()->m_ListOfX.size() >= m_iBufferPos && m_iBufferPos > 0) x_position = m_CharacterPositions.begin()->m_ListOfX[m_iBufferPos-1]; // Get complete length: x_total = m_CharacterPositions.begin()->m_ListOfX[m_CharacterPositions.begin()->m_ListOfX.size()-1]; } // Check if outside to the right if (x_position - m_HorizontalScroll + m_BufferZone * 2.f > m_CachedActualSize.GetWidth()) m_HorizontalScroll = x_position - m_CachedActualSize.GetWidth() + m_BufferZone * 2.f; // Check if outside to the left if (x_position - m_HorizontalScroll < 0.f) m_HorizontalScroll = x_position; // Check if the text doesn't even fill up to the right edge even though scrolling is done. if (m_HorizontalScroll != 0.f && x_total - m_HorizontalScroll + m_BufferZone * 2.f < m_CachedActualSize.GetWidth()) m_HorizontalScroll = x_total - m_CachedActualSize.GetWidth() + m_BufferZone * 2.f; // Now this is the fail-safe, if x_total isn't even the length of the control, // remove all scrolling if (x_total + m_BufferZone * 2.f < m_CachedActualSize.GetWidth()) m_HorizontalScroll = 0.f; } } Index: ps/trunk/source/gui/ObjectTypes/CInput.h =================================================================== --- ps/trunk/source/gui/ObjectTypes/CInput.h (revision 26869) +++ ps/trunk/source/gui/ObjectTypes/CInput.h (revision 26870) @@ -1,239 +1,241 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 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_CINPUT #define INCLUDED_CINPUT #include "gui/CGUISprite.h" #include "gui/ObjectBases/IGUIObject.h" #include "gui/ObjectBases/IGUIScrollBarOwner.h" #include "gui/SettingTypes/CGUIString.h" #include "lib/external_libraries/libsdl.h" #include #include /** * Text field where you can input and edit the text. * * It doesn't use IGUITextOwner, because we don't need * any other features than word-wrapping, and we need to be * able to rapidly change the string. */ class CInput : public IGUIObject, public IGUIScrollBarOwner { GUI_OBJECT(CInput) protected: // forwards struct SRow; public: CInput(CGUI& pGUI); virtual ~CInput(); /** * @see IGUIObject#ResetStates() */ virtual void ResetStates(); // Check where the mouse is hovering, and get the appropriate text position. // return is the text-position index. int GetMouseHoveringTextPosition() const; // Same as above, but only on one row in X, and a given value, not the mouse's. // wanted is filled with x if the row didn't extend as far as the mouse pos. int GetXTextPosition(const std::list::const_iterator& c, const float& x, float& wanted) const; protected: void SetupGeneratedPlaceholderText(); /** * @see IGUIObject#HandleMessage() */ virtual void HandleMessage(SGUIMessage& Message); /** * Handle events manually to catch keyboard inputting. */ virtual InReaction ManuallyHandleKeys(const SDL_Event_* ev); /** * Handle events manually to catch keys which change the text. */ virtual void ManuallyMutableHandleKeyDownEvent(const SDL_Keycode keyCode); /** * Handle events manually to catch keys which don't change the text. */ virtual void ManuallyImmutableHandleKeyDownEvent(const SDL_Keycode keyCode); /** * Handle hotkey events (called by ManuallyHandleKeys) */ virtual InReaction ManuallyHandleHotkeyEvent(const SDL_Event_* ev); /** * @see IGUIObject#UpdateCachedSize() */ virtual void UpdateCachedSize(); /** * Draws the Text */ virtual void Draw(CCanvas2D& canvas); + void DrawContent(CCanvas2D& canvas); + /** * Calculate m_CharacterPosition * the main task for this function is to perfom word-wrapping * You input from which character it has been changed, because * if we add a character to the very last end, we don't want * process everything all over again! Also notice you can * specify a 'to' also, it will only be used though if a '\n' * appears, because then the word-wrapping won't change after * that. */ void UpdateText(int from = 0, int to_before = -1, int to_after = -1); /** * Draws the text generated for placeholder. * * @param canvas Canvas to draw on. * @param clipping Clipping rectangle, don't even add a parameter * to get no clipping. */ virtual void DrawPlaceholderText(CCanvas2D& canvas, const CRect& clipping = CRect()); /** * Delete the current selection. Also places the pointer at the * crack between the two segments kept. */ void DeleteCurSelection(); /** * Is text selected? It can be denote two ways, m_iBufferPos_Tail * being -1 or the same as m_iBufferPos. This makes for clearer * code. */ bool SelectingText() const; /// Get area of where text can be drawn. float GetTextAreaWidth(); /// Called every time the auto-scrolling should be checked. void UpdateAutoScroll(); /// Clear composed IME input when supported (SDL2 only). void ClearComposedText(); /// Updates the buffer (cursor) position exposed to JS. void UpdateBufferPositionSetting(); protected: /// Cursor position int m_iBufferPos; /// Cursor position we started to select from. (-1 if not selecting) /// (NB: Can be larger than m_iBufferPos if selecting from back to front.) int m_iBufferPos_Tail; /// If we're composing text with an IME bool m_ComposingText; /// The length and position of the current IME composition int m_iComposedLength, m_iComposedPos; /// The position to insert committed text int m_iInsertPos; // the outer vector is lines, and the inner is X positions // in a row. So that we can determine where characters are // placed. It's important because we need to know where the // pointer should be placed when the input control is pressed. struct SRow { // Where the Row starts int m_ListStart; // List of X values for each character. std::vector m_ListOfX; }; /** * List of rows to ease changing its size, so iterators stay valid. * For one-liners only one row is used. */ std::list m_CharacterPositions; // *** Things for a multi-lined input control *** // /** * When you change row with up/down, and the row you jump to does * not have anything at that X position, then it will keep the * m_WantedX position in mind when switching to the next row. * It will keep on being used until it reach a row which meets the * requirements. * 0.0f means not in use. */ float m_WantedX; /** * If we are in the process of selecting a larger selection of text * using the mouse click (hold) and drag, this is true. */ bool m_SelectingText; /** * Whether the cached text is currently valid (if not then SetupText will be called by Draw) */ bool m_GeneratedPlaceholderTextValid; CGUIText m_GeneratedPlaceholderText; // *** Things for one-line input control *** // float m_HorizontalScroll; /// Used to store the previous time for flashing the cursor. double m_PrevTime; /// Cursor blink rate in seconds, if greater than 0.0. double m_CursorBlinkRate; /// If the cursor should be drawn or not. bool m_CursorVisState; static const CStr EventNameTextEdit; static const CStr EventNamePress; static const CStr EventNameTab; CGUISimpleSetting m_BufferPosition; CGUISimpleSetting m_BufferZone; CGUISimpleSetting m_Caption; CGUISimpleSetting m_PlaceholderText; CGUISimpleSetting m_Font; CGUISimpleSetting m_MaskChar; CGUISimpleSetting m_Mask; CGUISimpleSetting m_MaxLength; CGUISimpleSetting m_MultiLine; CGUISimpleSetting m_Readonly; CGUISimpleSetting m_ScrollBar; CGUISimpleSetting m_ScrollBarStyle; CGUISimpleSetting m_Sprite; CGUISimpleSetting m_SpriteOverlay; CGUISimpleSetting m_SpriteSelectArea; CGUISimpleSetting m_TextColor; CGUISimpleSetting m_TextColorSelected; CGUISimpleSetting m_PlaceholderColor; }; #endif // INCLUDED_CINPUT