// Copyright (C) 2022 Jérôme "Lynix" Leclercq (lynix680@gmail.com) // This file is part of the "Nazara Engine - Widgets module" // For conditions of distribution and use, see copyright notice in Config.hpp #include #include #include #include #include #include #include namespace Nz { namespace NAZARA_ANONYMOUS_NAMESPACE { bool IsSpace(char32_t character) { switch (character) { case '\f': case '\n': case '\r': case '\t': case '\v': return true; default: return Unicode::GetCategory(character) & Unicode::Category_Separator; } } } TextAreaWidget::TextAreaWidget(BaseWidget* parent) : AbstractTextAreaWidget(parent) { SetCharacterSize(GetCharacterSize()); //< Actualize minimum / preferred size Layout(); } void TextAreaWidget::AppendText(const std::string& text) { m_text += text; switch (m_echoMode) { case EchoMode::Normal: m_drawer.AppendText(text); break; case EchoMode::Hidden: m_drawer.AppendText(std::string(ComputeCharacterCount(text), '*')); break; case EchoMode::HiddenExceptLast: { m_drawer.Clear(); std::size_t textLength = ComputeCharacterCount(m_text); if (textLength >= 2) { std::size_t lastCharacterPosition = GetCharacterPosition(m_text, textLength - 2); if (lastCharacterPosition != std::string::npos) m_drawer.AppendText(std::string(textLength - 1, '*')); } if (textLength >= 1) m_drawer.AppendText(m_text.substr(GetCharacterPosition(m_text, textLength - 1))); //< FIXME: getting last character position could be massively optimized break; } } UpdateTextSprite(); OnTextChanged(this, m_text); } void TextAreaWidget::Clear() { AbstractTextAreaWidget::Clear(); m_text.clear(); OnTextChanged(this, m_text); } void TextAreaWidget::Erase(std::size_t firstGlyph, std::size_t lastGlyph) { if (firstGlyph > lastGlyph) std::swap(firstGlyph, lastGlyph); std::size_t textLength = ComputeCharacterCount(m_text); if (firstGlyph > textLength) return; std::string newText; if (firstGlyph > 0) { std::size_t characterPosition = GetCharacterPosition(m_text, firstGlyph); NazaraAssert(characterPosition != std::string::npos, "Invalid character position"); newText.append(m_text.substr(0, characterPosition)); } if (lastGlyph < textLength) { std::size_t characterPosition = GetCharacterPosition(m_text, lastGlyph); NazaraAssert(characterPosition != std::string::npos, "Invalid character position"); newText.append(m_text.substr(characterPosition)); } SetText(newText); } void TextAreaWidget::Write(const std::string& text, std::size_t glyphPosition) { if (glyphPosition >= m_drawer.GetGlyphCount()) { // It's faster to append than to insert in the middle AppendText(text); SetCursorPosition(m_drawer.GetGlyphCount()); } else { m_text.insert(GetCharacterPosition(m_text, glyphPosition), text); SetText(m_text); SetCursorPosition(glyphPosition + ComputeCharacterCount(text)); } } void TextAreaWidget::CopySelectionToClipboard(const Vector2ui& selectionBegin, const Vector2ui& selectionEnd) { std::size_t glyphCount = ComputeCharacterCount(m_text); assert(glyphCount > 0); std::size_t startIndex = GetCharacterPosition(m_text, GetGlyphIndex(selectionBegin)); std::size_t endIndex = GetCharacterPosition(m_text, std::min(GetGlyphIndex(selectionEnd), glyphCount)); Clipboard::SetString(m_text.substr(startIndex, endIndex - startIndex)); } AbstractTextDrawer& TextAreaWidget::GetTextDrawer() { return m_drawer; } const AbstractTextDrawer& TextAreaWidget::GetTextDrawer() const { return m_drawer; } void TextAreaWidget::HandleIndentation(bool add) { if (add) Write("\t"); else { std::size_t currentGlyph = GetGlyphIndex(m_cursorPositionBegin); if (currentGlyph > 0 && m_text[GetCharacterPosition(m_text, currentGlyph - 1U)] == '\t') // Check if previous glyph is a tab { Erase(currentGlyph - 1U); if (m_cursorPositionBegin.x < static_cast(m_drawer.GetLineGlyphCount(m_cursorPositionBegin.y))) MoveCursor(-1); } } } void TextAreaWidget::HandleSelectionIndentation(bool add) { for (unsigned line = m_cursorPositionBegin.y; line <= m_cursorPositionEnd.y; ++line) { const Vector2ui cursorPositionBegin = m_cursorPositionBegin; const Vector2ui cursorPositionEnd = m_cursorPositionEnd; if (add) { Write("\t", {0U, line}); SetSelection(cursorPositionBegin + (cursorPositionBegin.y == line && cursorPositionBegin.x != 0U ? Vector2ui{ 1U, 0U } : Vector2ui{}), cursorPositionEnd + (cursorPositionEnd.y == line ? Vector2ui{ 1U, 0U } : Vector2ui{})); } else { if (m_drawer.GetLineGlyphCount(line) == 0) continue; std::size_t firstGlyph = GetGlyphIndex({ 0U, line }); if (m_text[GetCharacterPosition(m_text, firstGlyph)] == '\t') { Erase(firstGlyph); SetSelection(cursorPositionBegin - (cursorPositionBegin.y == line && cursorPositionBegin.x != 0U ? Vector2ui{ 1U, 0U } : Vector2ui{}), cursorPositionEnd - (cursorPositionEnd.y == line && cursorPositionEnd.x != 0U ? Vector2ui{ 1U, 0U } : Vector2ui{})); } } } } void TextAreaWidget::HandleWordCursorMove(bool left) { if (left) { std::size_t index = GetGlyphIndex(m_cursorPositionBegin); if (index == 0) return; std::size_t spaceIndex = m_text.rfind(' ', index - 1); std::size_t endlIndex = m_text.rfind('\n', index - 1); if ((spaceIndex > endlIndex || endlIndex == std::string::npos) && spaceIndex != std::string::npos) SetCursorPosition(spaceIndex + 1); else if (endlIndex != std::string::npos) { if (index == endlIndex + 1) SetCursorPosition(endlIndex); else SetCursorPosition(endlIndex + 1); } else SetCursorPosition({ 0U, m_cursorPositionBegin.y }); } else { std::size_t index = GetGlyphIndex(m_cursorPositionEnd); std::size_t spaceIndex = m_text.find(' ', index); std::size_t endlIndex = m_text.find('\n', index); if (spaceIndex < endlIndex && spaceIndex != std::string::npos) { if (ComputeCharacterCount(m_text) > spaceIndex) SetCursorPosition(spaceIndex + 1); else SetCursorPosition({ static_cast(m_drawer.GetLineGlyphCount(m_cursorPositionEnd.y)), m_cursorPositionEnd.y }); } else if (endlIndex != std::string::npos) { if (index == endlIndex) SetCursorPosition(endlIndex + 1); else SetCursorPosition(endlIndex); } else SetCursorPosition({ static_cast(m_drawer.GetLineGlyphCount(m_cursorPositionEnd.y)), m_cursorPositionEnd.y }); } } void TextAreaWidget::HandleWordSelection(const Vector2ui& position) { NAZARA_USE_ANONYMOUS_NAMESPACE std::size_t index = GetGlyphIndex(m_cursorPositionEnd); // FIXME: Handle Unicode properly std::size_t startIndex = index; for (std::string::reverse_iterator it { m_text.begin() + index }; it != m_text.rend(); ++it) { if (IsSpace(*it)) break; --startIndex; } std::size_t endIndex = index; for (auto it = m_text.begin() + index; it != m_text.end(); ++it) { if (IsSpace(*it)) break; ++endIndex; } SetSelection(GetCursorPosition(GetCharacterPosition(m_text, startIndex)), GetCursorPosition(GetCharacterPosition(m_text, endIndex))); } void TextAreaWidget::PasteFromClipboard(const Vector2ui& targetPosition) { std::size_t glyphCount = ComputeCharacterCount(m_text); std::size_t targetIndex = GetCharacterPosition(m_text, std::min(GetGlyphIndex(targetPosition), glyphCount)); std::string clipboardString = Clipboard::GetString(); if (clipboardString.empty()) return; m_text.insert(targetIndex, clipboardString); UpdateDisplayText(); SetCursorPosition(targetIndex + ComputeCharacterCount(clipboardString)); } void TextAreaWidget::UpdateDisplayText() { switch (m_echoMode) { case EchoMode::Normal: m_drawer.SetText(m_text); break; case EchoMode::Hidden: case EchoMode::HiddenExceptLast: m_drawer.SetText(std::string(ComputeCharacterCount(m_text), '*')); break; } UpdateTextSprite(); SetCursorPosition(m_cursorPositionBegin); //< Refresh cursor position (prevent it from being outside of the text) } void TextAreaWidget::UpdateMinimumSize() { std::size_t fontCount = m_drawer.GetFontCount(); float lineHeight = 0; int spaceAdvance = 0; for (std::size_t i = 0; i < fontCount; ++i) { const std::shared_ptr& font = m_drawer.GetFont(i); const Font::SizeInfo& sizeInfo = font->GetSizeInfo(m_drawer.GetCharacterSize()); lineHeight = std::max(lineHeight, m_drawer.GetLineHeight()); spaceAdvance = std::max(spaceAdvance, sizeInfo.spaceAdvance); } Vector2f size = { float(spaceAdvance), lineHeight + 5.f }; SetMinimumSize(size); } }