diff --git a/ChangeLog.md b/ChangeLog.md index 6b1641293..51476899b 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -88,6 +88,7 @@ Nazara Engine: - Graphics module now register "White2D" and "WhiteCubemap" textures to the TextureLibrary (respectively a 1x1 texture 2D and a 1x1 texture cubemap) - Added AbstractTextDrawer::GetLineGlyphCount, which returns the number of glyph part of the line - Fixed Font handling of whitespace glyphs (which were triggering an error) +- ⚠️ Translucent2D pipeline no longer has depth sorting Nazara Development Kit: - Added ImageWidget (#139) @@ -132,6 +133,8 @@ Nazara Development Kit: - ⚠️ Rewrote all render queue system, which should be more efficient, take scissor box into account - ⚠️ All widgets are now bound to a scissor box when rendering - Add DebugComponent (a component able to show aabb/obb/collision mesh) +- ⚠️ TextAreaWidget now support text selection (WIP) +- ⚠️ TextAreaWidget::GetHoveredGlyph now returns a two-dimensional position instead of a single glyph position # 0.4: diff --git a/SDK/include/NDK/Widgets/TextAreaWidget.hpp b/SDK/include/NDK/Widgets/TextAreaWidget.hpp index e0f08053e..e7cdc2f7a 100644 --- a/SDK/include/NDK/Widgets/TextAreaWidget.hpp +++ b/SDK/include/NDK/Widgets/TextAreaWidget.hpp @@ -11,6 +11,7 @@ #include #include #include +#include namespace Ndk { @@ -30,15 +31,20 @@ namespace Ndk inline void EnableMultiline(bool enable = true); + void EraseSelection(); + inline unsigned int GetCharacterSize() const; inline const Nz::Vector2ui& GetCursorPosition() const; + inline Nz::Vector2ui GetCursorPosition(std::size_t glyphIndex) const; inline const Nz::String& GetDisplayText() const; inline EchoMode GetEchoMode() const; inline std::size_t GetGlyphIndex(const Nz::Vector2ui& cursorPosition); inline const Nz::String& GetText() const; inline const Nz::Color& GetTextColor() const; - std::size_t GetHoveredGlyph(float x, float y) const; + Nz::Vector2ui GetHoveredGlyph(float x, float y) const; + + inline bool HasSelection() const; inline bool IsMultilineEnabled() const; inline bool IsReadOnly() const; @@ -53,6 +59,7 @@ namespace Ndk inline void SetCursorPosition(Nz::Vector2ui cursorPosition); inline void SetEchoMode(EchoMode echoMode); inline void SetReadOnly(bool readOnly = true); + inline void SetSelection(Nz::Vector2ui fromPosition, Nz::Vector2ui toPosition); inline void SetText(const Nz::String& text); inline void SetTextColor(const Nz::Color& text); @@ -64,6 +71,8 @@ namespace Ndk NazaraSignal(OnTextAreaCursorMove, const TextAreaWidget* /*textArea*/, std::size_t* /*newCursorPosition*/); NazaraSignal(OnTextAreaKeyBackspace, const TextAreaWidget* /*textArea*/, bool* /*ignoreDefaultAction*/); NazaraSignal(OnTextAreaKeyDown, const TextAreaWidget* /*textArea*/, bool* /*ignoreDefaultAction*/); + NazaraSignal(OnTextAreaKeyEnd, const TextAreaWidget* /*textArea*/, bool* /*ignoreDefaultAction*/); + NazaraSignal(OnTextAreaKeyHome, const TextAreaWidget* /*textArea*/, bool* /*ignoreDefaultAction*/); NazaraSignal(OnTextAreaKeyLeft, const TextAreaWidget* /*textArea*/, bool* /*ignoreDefaultAction*/); NazaraSignal(OnTextAreaKeyReturn, const TextAreaWidget* /*textArea*/, bool* /*ignoreDefaultAction*/); NazaraSignal(OnTextAreaKeyRight, const TextAreaWidget* /*textArea*/, bool* /*ignoreDefaultAction*/); @@ -79,6 +88,9 @@ namespace Ndk bool OnKeyPressed(const Nz::WindowEvent::KeyEvent& key) override; void OnKeyReleased(const Nz::WindowEvent::KeyEvent& key) override; void OnMouseButtonPress(int /*x*/, int /*y*/, Nz::Mouse::Button button) override; + void OnMouseButtonRelease(int /*x*/, int /*y*/, Nz::Mouse::Button button) override; + void OnMouseEnter() override; + void OnMouseMoved(int x, int y, int deltaX, int deltaY) override; void OnTextEntered(char32_t character, bool repeated) override; void RefreshCursor(); @@ -88,10 +100,13 @@ namespace Ndk EntityHandle m_cursorEntity; EntityHandle m_textEntity; Nz::SimpleTextDrawer m_drawer; - Nz::SpriteRef m_cursorSprite; Nz::String m_text; Nz::TextSpriteRef m_textSprite; - Nz::Vector2ui m_cursorPosition; + Nz::Vector2ui m_cursorPositionBegin; + Nz::Vector2ui m_cursorPositionEnd; + Nz::Vector2ui m_selectionCursor; + std::vector m_cursorSprites; + bool m_isMouseButtonDown; bool m_multiLineEnabled; bool m_readOnly; }; diff --git a/SDK/include/NDK/Widgets/TextAreaWidget.inl b/SDK/include/NDK/Widgets/TextAreaWidget.inl index 07daf7896..274d46a02 100644 --- a/SDK/include/NDK/Widgets/TextAreaWidget.inl +++ b/SDK/include/NDK/Widgets/TextAreaWidget.inl @@ -8,7 +8,8 @@ namespace Ndk { inline void TextAreaWidget::Clear() { - m_cursorPosition.MakeZero(); + m_cursorPositionBegin.MakeZero(); + m_cursorPositionEnd.MakeZero(); m_drawer.Clear(); m_text.Clear(); m_textSprite->Update(m_drawer); @@ -29,7 +30,30 @@ namespace Ndk inline const Nz::Vector2ui& TextAreaWidget::GetCursorPosition() const { - return m_cursorPosition; + return m_cursorPositionBegin; + } + + Nz::Vector2ui TextAreaWidget::GetCursorPosition(std::size_t glyphIndex) const + { + glyphIndex = std::min(glyphIndex, m_drawer.GetGlyphCount()); + + std::size_t lineCount = m_drawer.GetLineCount(); + std::size_t line = 0U; + for (std::size_t i = line + 1; i < lineCount; ++i) + { + if (m_drawer.GetLine(i).glyphIndex > glyphIndex) + break; + + line = i; + } + + const auto& lineInfo = m_drawer.GetLine(line); + + Nz::Vector2ui cursorPos; + cursorPos.y = static_cast(line); + cursorPos.x = static_cast(glyphIndex - lineInfo.glyphIndex); + + return cursorPos; } inline const Nz::String& TextAreaWidget::GetDisplayText() const @@ -63,7 +87,12 @@ namespace Ndk return m_drawer.GetColor(); } - inline bool Ndk::TextAreaWidget::IsMultilineEnabled() const + inline bool TextAreaWidget::HasSelection() const + { + return m_cursorPositionBegin != m_cursorPositionEnd; + } + + inline bool TextAreaWidget::IsMultilineEnabled() const { return m_multiLineEnabled; } @@ -75,7 +104,7 @@ namespace Ndk inline void TextAreaWidget::MoveCursor(int offset) { - std::size_t cursorGlyph = GetGlyphIndex(m_cursorPosition); + std::size_t cursorGlyph = GetGlyphIndex(m_cursorPositionBegin); if (offset >= 0) SetCursorPosition(cursorGlyph + static_cast(offset)); else @@ -104,7 +133,7 @@ namespace Ndk } }; - Nz::Vector2ui cursorPosition = m_cursorPosition; + Nz::Vector2ui cursorPosition = m_cursorPositionBegin; cursorPosition.x = ClampOffset(static_cast(cursorPosition.x), offset.x); cursorPosition.y = ClampOffset(static_cast(cursorPosition.y), offset.y); @@ -120,22 +149,8 @@ namespace Ndk { OnTextAreaCursorMove(this, &glyphIndex); - glyphIndex = std::min(glyphIndex, m_drawer.GetGlyphCount()); - - std::size_t lineCount = m_drawer.GetLineCount(); - std::size_t line = 0U; - for (std::size_t i = line + 1; i < lineCount; ++i) - { - if (m_drawer.GetLine(i).glyphIndex > glyphIndex) - break; - - line = i; - } - - const auto& lineInfo = m_drawer.GetLine(line); - - m_cursorPosition.y = static_cast(line); - m_cursorPosition.x = static_cast(glyphIndex - lineInfo.glyphIndex); + m_cursorPositionBegin = GetCursorPosition(glyphIndex); + m_cursorPositionEnd = m_cursorPositionBegin; RefreshCursor(); } @@ -146,7 +161,7 @@ namespace Ndk if (cursorPosition.y >= lineCount) cursorPosition.y = static_cast(lineCount - 1); - m_cursorPosition = cursorPosition; + m_cursorPositionBegin = cursorPosition; const auto& lineInfo = m_drawer.GetLine(cursorPosition.y); if (cursorPosition.y + 1 < lineCount) @@ -155,6 +170,8 @@ namespace Ndk cursorPosition.x = std::min(cursorPosition.x, static_cast(nextLineInfo.glyphIndex - lineInfo.glyphIndex - 1)); } + m_cursorPositionEnd = m_cursorPositionBegin; + std::size_t glyphIndex = lineInfo.glyphIndex + cursorPosition.x; OnTextAreaCursorMove(this, &glyphIndex); @@ -175,6 +192,23 @@ namespace Ndk m_cursorEntity->Enable(!m_readOnly && HasFocus()); } + inline void TextAreaWidget::SetSelection(Nz::Vector2ui fromPosition, Nz::Vector2ui toPosition) + { + ///TODO: Check if position are valid + + // Ensure begin is before end + if (toPosition.y < fromPosition.y || (toPosition.y == fromPosition.y && toPosition.x < fromPosition.x)) + std::swap(fromPosition, toPosition); + + if (m_cursorPositionBegin != fromPosition || m_cursorPositionEnd != toPosition) + { + m_cursorPositionBegin = fromPosition; + m_cursorPositionEnd = toPosition; + + RefreshCursor(); + } + } + inline void TextAreaWidget::SetText(const Nz::String& text) { m_text = text; diff --git a/SDK/src/NDK/Widgets/TextAreaWidget.cpp b/SDK/src/NDK/Widgets/TextAreaWidget.cpp index b750dd2f2..764a328d8 100644 --- a/SDK/src/NDK/Widgets/TextAreaWidget.cpp +++ b/SDK/src/NDK/Widgets/TextAreaWidget.cpp @@ -12,16 +12,14 @@ namespace Ndk TextAreaWidget::TextAreaWidget(BaseWidget* parent) : BaseWidget(parent), m_echoMode(EchoMode_Normal), - m_cursorPosition(0U, 0U), + m_cursorPositionBegin(0U, 0U), + m_cursorPositionEnd(0U, 0U), + m_isMouseButtonDown(false), m_multiLineEnabled(false), m_readOnly(false) { - m_cursorSprite = Nz::Sprite::New(); - m_cursorSprite->SetColor(Nz::Color::Black); - m_cursorSprite->SetSize(1.f, float(m_drawer.GetFont()->GetSizeInfo(m_drawer.GetCharacterSize()).lineHeight)); - m_cursorEntity = CreateEntity(true); - m_cursorEntity->AddComponent().Attach(m_cursorSprite, 10); + m_cursorEntity->AddComponent(); m_cursorEntity->AddComponent().SetParent(this); m_cursorEntity->Enable(false); @@ -72,7 +70,29 @@ namespace Ndk OnTextChanged(this, m_text); } - std::size_t TextAreaWidget::GetHoveredGlyph(float x, float y) const + void TextAreaWidget::EraseSelection() + { + if (!HasSelection()) + return; + + std::size_t cursorGlyphBegin = GetGlyphIndex(m_cursorPositionBegin); + std::size_t cursorGlyphEnd = GetGlyphIndex(m_cursorPositionEnd); + + std::size_t textLength = m_text.GetLength(); + if (cursorGlyphBegin > textLength) + return; + + Nz::String newText; + if (cursorGlyphBegin > 0) + newText.Append(m_text.SubString(0, m_text.GetCharacterPosition(cursorGlyphBegin) - 1)); + + if (cursorGlyphEnd < textLength) + newText.Append(m_text.SubString(m_text.GetCharacterPosition(cursorGlyphEnd))); + + SetText(newText); + } + + Nz::Vector2ui TextAreaWidget::GetHoveredGlyph(float x, float y) const { std::size_t glyphCount = m_drawer.GetGlyphCount(); if (glyphCount > 0) @@ -88,7 +108,8 @@ namespace Ndk std::size_t upperLimit = (line != lineCount - 1) ? m_drawer.GetLine(line + 1).glyphIndex : glyphCount + 1; - std::size_t i = m_drawer.GetLine(line).glyphIndex; + std::size_t firstLineGlyph = m_drawer.GetLine(line).glyphIndex; + std::size_t i = firstLineGlyph; for (; i < upperLimit - 1; ++i) { Nz::Rectf bounds = m_drawer.GetGlyph(i).bounds; @@ -96,10 +117,10 @@ namespace Ndk break; } - return i; + return Nz::Vector2ui(i - firstLineGlyph, line); } - return 0; + return Nz::Vector2ui::Zero(); } void TextAreaWidget::ResizeToContent() @@ -109,7 +130,7 @@ namespace Ndk void TextAreaWidget::Write(const Nz::String& text) { - std::size_t cursorGlyph = GetGlyphIndex(m_cursorPosition); + std::size_t cursorGlyph = GetGlyphIndex(m_cursorPositionBegin); if (cursorGlyph >= m_drawer.GetGlyphCount()) { @@ -156,20 +177,27 @@ namespace Ndk { case Nz::Keyboard::Delete: { - std::size_t cursorGlyph = GetGlyphIndex(m_cursorPosition); + if (HasSelection()) + EraseSelection(); + else + { + std::size_t cursorGlyphBegin = GetGlyphIndex(m_cursorPositionBegin); + std::size_t cursorGlyphEnd = GetGlyphIndex(m_cursorPositionEnd); - std::size_t textLength = m_text.GetLength(); - if (cursorGlyph > textLength) - return true; + std::size_t textLength = m_text.GetLength(); + if (cursorGlyphBegin > textLength) + return true; - Nz::String newText; - if (cursorGlyph > 0) - newText.Append(m_text.SubString(0, m_text.GetCharacterPosition(cursorGlyph) - 1)); + Nz::String newText; + if (cursorGlyphBegin > 0) + newText.Append(m_text.SubString(0, m_text.GetCharacterPosition(cursorGlyphBegin) - 1)); - if (cursorGlyph < textLength) - newText.Append(m_text.SubString(m_text.GetCharacterPosition(cursorGlyph + 1))); + if (cursorGlyphEnd < textLength) + newText.Append(m_text.SubString(m_text.GetCharacterPosition(cursorGlyphEnd + 1))); + + SetText(newText); + } - SetText(newText); return true; } @@ -181,10 +209,38 @@ namespace Ndk if (ignoreDefaultAction) return true; + if (HasSelection()) + SetCursorPosition(m_cursorPositionEnd); + MoveCursor({0, 1}); return true; } + case Nz::Keyboard::End: + { + bool ignoreDefaultAction = false; + OnTextAreaKeyEnd(this, &ignoreDefaultAction); + + if (ignoreDefaultAction) + return true; + + const auto& lineInfo = m_drawer.GetLine(m_cursorPositionEnd.y); + SetCursorPosition({ m_drawer.GetLineGlyphCount(m_cursorPositionEnd.y), m_cursorPositionEnd.y }); + return true; + } + + case Nz::Keyboard::Home: + { + bool ignoreDefaultAction = false; + OnTextAreaKeyHome(this, &ignoreDefaultAction); + + if (ignoreDefaultAction) + return true; + + SetCursorPosition({ 0U, m_cursorPositionEnd.y }); + return true; + } + case Nz::Keyboard::Left: { bool ignoreDefaultAction = false; @@ -193,7 +249,11 @@ namespace Ndk if (ignoreDefaultAction) return true; - MoveCursor(-1); + if (HasSelection()) + SetCursorPosition(m_cursorPositionBegin); + else + MoveCursor(-1); + return true; } @@ -205,7 +265,11 @@ namespace Ndk if (ignoreDefaultAction) return true; - MoveCursor(1); + if (HasSelection()) + SetCursorPosition(m_cursorPositionEnd); + else + MoveCursor(1); + return true; } @@ -217,6 +281,9 @@ namespace Ndk if (ignoreDefaultAction) return true; + if (HasSelection()) + SetCursorPosition(m_cursorPositionBegin); + MoveCursor({0, -1}); return true; } @@ -237,7 +304,39 @@ namespace Ndk SetFocus(); const Padding& padding = GetPadding(); - SetCursorPosition(GetHoveredGlyph(float(x - padding.left), float(y - padding.top))); + Nz::Vector2ui hoveredGlyph = GetHoveredGlyph(float(x - padding.left), float(y - padding.top)); + + // Shift extends selection + if (Nz::Keyboard::IsKeyPressed(Nz::Keyboard::LShift) || Nz::Keyboard::IsKeyPressed(Nz::Keyboard::RShift)) + SetSelection(hoveredGlyph, m_selectionCursor); + else + { + SetCursorPosition(hoveredGlyph); + m_selectionCursor = m_cursorPositionBegin; + } + + m_isMouseButtonDown = true; + } + } + + void TextAreaWidget::OnMouseButtonRelease(int, int, Nz::Mouse::Button button) + { + if (button == Nz::Mouse::Left) + m_isMouseButtonDown = false; + } + + void TextAreaWidget::OnMouseEnter() + { + if (!Nz::Mouse::IsButtonPressed(Nz::Mouse::Left)) + m_isMouseButtonDown = false; + } + + void TextAreaWidget::OnMouseMoved(int x, int y, int deltaX, int deltaY) + { + if (m_isMouseButtonDown) + { + const Padding& padding = GetPadding(); + SetSelection(m_selectionCursor, GetHoveredGlyph(float(x - padding.left), float(y - padding.top))); } } @@ -253,20 +352,30 @@ namespace Ndk bool ignoreDefaultAction = false; OnTextAreaKeyBackspace(this, &ignoreDefaultAction); - std::size_t cursorGlyph = GetGlyphIndex(m_cursorPosition); - if (ignoreDefaultAction || cursorGlyph == 0) + std::size_t cursorGlyphBegin = GetGlyphIndex(m_cursorPositionBegin); + std::size_t cursorGlyphEnd = GetGlyphIndex(m_cursorPositionEnd); + + if (ignoreDefaultAction || cursorGlyphEnd == 0) break; - Nz::String newText; + // When a text is selected, delete key does the same as delete and leave the character behind it + if (HasSelection()) + EraseSelection(); + else + { + Nz::String newText; - if (cursorGlyph > 1) - newText.Append(m_text.SubString(0, m_text.GetCharacterPosition(cursorGlyph - 1) - 1)); + if (cursorGlyphBegin > 1) + newText.Append(m_text.SubString(0, m_text.GetCharacterPosition(cursorGlyphBegin - 1) - 1)); - if (cursorGlyph < m_text.GetLength()) - newText.Append(m_text.SubString(m_text.GetCharacterPosition(cursorGlyph))); + if (cursorGlyphEnd < m_text.GetLength()) + newText.Append(m_text.SubString(m_text.GetCharacterPosition(cursorGlyphEnd))); - MoveCursor(-1); - SetText(newText); + // Move cursor before setting text (to prevent SetText to move our cursor) + MoveCursor(-1); + + SetText(newText); + } break; } @@ -288,6 +397,9 @@ namespace Ndk if (Nz::Unicode::GetCategory(character) == Nz::Unicode::Category_Other_Control) break; + if (HasSelection()) + EraseSelection(); + Write(Nz::String::Unicode(character)); break; } @@ -299,24 +411,68 @@ namespace Ndk if (m_readOnly) return; - const auto& lineInfo = m_drawer.GetLine(m_cursorPosition.y); - std::size_t cursorGlyph = GetGlyphIndex(m_cursorPosition); + m_cursorEntity->GetComponent().SetPosition(GetContentOrigin()); - std::size_t glyphCount = m_drawer.GetGlyphCount(); - float position; - if (glyphCount > 0 && lineInfo.glyphIndex < cursorGlyph) + std::size_t selectionLineCount = m_cursorPositionEnd.y - m_cursorPositionBegin.y + 1; + std::size_t oldSpriteCount = m_cursorSprites.size(); + if (m_cursorSprites.size() != selectionLineCount) { - const auto& glyph = m_drawer.GetGlyph(std::min(cursorGlyph, glyphCount - 1)); - position = glyph.bounds.x; - if (cursorGlyph >= glyphCount) - position += glyph.bounds.width; + m_cursorSprites.resize(m_cursorPositionEnd.y - m_cursorPositionBegin.y + 1); + for (std::size_t i = oldSpriteCount; i < m_cursorSprites.size(); ++i) + { + m_cursorSprites[i] = Nz::Sprite::New(); + m_cursorSprites[i]->SetMaterial(Nz::Material::New("Translucent2D")); + } } - else - position = 0.f; - Nz::Vector2f contentOrigin = GetContentOrigin(); + float lineHeight = float(m_drawer.GetFont()->GetSizeInfo(m_drawer.GetCharacterSize()).lineHeight); - m_cursorEntity->GetComponent().SetPosition(contentOrigin.x + position, contentOrigin.y + lineInfo.bounds.y); + GraphicsComponent& gfxComponent = m_cursorEntity->GetComponent(); + gfxComponent.Clear(); + + for (unsigned int i = m_cursorPositionBegin.y; i <= m_cursorPositionEnd.y; ++i) + { + const auto& lineInfo = m_drawer.GetLine(i); + + Nz::SpriteRef& cursorSprite = m_cursorSprites[i - m_cursorPositionBegin.y]; + if (i == m_cursorPositionBegin.y || i == m_cursorPositionEnd.y) + { + auto GetGlyphPos = [&](std::size_t localGlyphPos) + { + std::size_t cursorGlyph = GetGlyphIndex({ localGlyphPos, i }); + + std::size_t glyphCount = m_drawer.GetGlyphCount(); + float position; + if (glyphCount > 0 && lineInfo.glyphIndex < cursorGlyph) + { + const auto& glyph = m_drawer.GetGlyph(std::min(cursorGlyph, glyphCount - 1)); + position = glyph.bounds.x; + if (cursorGlyph >= glyphCount) + position += glyph.bounds.width; + } + else + position = 0.f; + + return position; + }; + + float beginX = (i == m_cursorPositionBegin.y) ? GetGlyphPos(m_cursorPositionBegin.x) : 0.f; + float endX = (i == m_cursorPositionEnd.y) ? GetGlyphPos(m_cursorPositionEnd.x) : lineInfo.bounds.width; + float spriteSize = std::max(endX - beginX, 1.f); + + cursorSprite->SetColor((m_cursorPositionBegin == m_cursorPositionEnd) ? Nz::Color::Black : Nz::Color(0, 0, 0, 50)); + cursorSprite->SetSize(spriteSize, float(m_drawer.GetFont()->GetSizeInfo(m_drawer.GetCharacterSize()).lineHeight)); + + gfxComponent.Attach(cursorSprite, Nz::Matrix4f::Translate({ beginX, lineInfo.bounds.y, 0.f })); + } + else + { + cursorSprite->SetColor(Nz::Color(0, 0, 0, 50)); + cursorSprite->SetSize(lineInfo.bounds.width, float(m_drawer.GetFont()->GetSizeInfo(m_drawer.GetCharacterSize()).lineHeight)); + + gfxComponent.Attach(cursorSprite, Nz::Matrix4f::Translate({ 0.f, lineInfo.bounds.y, 0.f })); + } + } } void TextAreaWidget::UpdateDisplayText() @@ -335,6 +491,6 @@ namespace Ndk m_textSprite->Update(m_drawer); - SetCursorPosition(m_cursorPosition); //< Refresh cursor position (prevent it from being outside of the text) + SetCursorPosition(m_cursorPositionBegin); //< Refresh cursor position (prevent it from being outside of the text) } }