diff --git a/ChangeLog.md b/ChangeLog.md index e3a07e3f0..07a90646e 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -283,6 +283,8 @@ Nazara Development Kit: - ⚠️ TextAreaWidget::OnTextAreaCursorMove signal now uses a Vector2ui* position as its second argument (instead of a std::size_t*) - Added TextAreaWidget::OnTextAreaSelection - ⚠️ Console class is no longer bound to a LuaState and now has a OnCommand signal +- ⚠️ Made AbstractTextAreaWidget which is inherited by TextAreaWidget +- ⚠️ Added RichTextAreaWidget # 0.4: diff --git a/SDK/include/NDK/Console.hpp b/SDK/include/NDK/Console.hpp index 32d0cd76a..796a15588 100644 --- a/SDK/include/NDK/Console.hpp +++ b/SDK/include/NDK/Console.hpp @@ -24,6 +24,7 @@ namespace Nz namespace Ndk { + class AbstractTextAreaWidget; class Console; class Entity; class ScrollAreaWidget; @@ -59,7 +60,7 @@ namespace Ndk NazaraSignal(OnCommand, Console* /*console*/, const Nz::String& /*command*/); private: - void ExecuteInput(const TextAreaWidget* textArea, bool* ignoreDefaultAction); + void ExecuteInput(const AbstractTextAreaWidget* textArea, bool* ignoreDefaultAction); void Layout() override; struct Line diff --git a/SDK/include/NDK/Widgets.hpp b/SDK/include/NDK/Widgets.hpp index 303b22519..9a36e37a2 100644 --- a/SDK/include/NDK/Widgets.hpp +++ b/SDK/include/NDK/Widgets.hpp @@ -5,6 +5,7 @@ #ifndef NDK_WIDGETS_GLOBAL_HPP #define NDK_WIDGETS_GLOBAL_HPP +#include #include #include #include @@ -12,6 +13,7 @@ #include #include #include +#include #include #include diff --git a/SDK/include/NDK/Widgets/AbstractTextAreaWidget.hpp b/SDK/include/NDK/Widgets/AbstractTextAreaWidget.hpp new file mode 100644 index 000000000..b05f65bce --- /dev/null +++ b/SDK/include/NDK/Widgets/AbstractTextAreaWidget.hpp @@ -0,0 +1,135 @@ +// Copyright (C) 2017 Jérôme Leclercq +// This file is part of the "Nazara Development Kit" +// For conditions of distribution and use, see copyright notice in Prerequisites.hpp + +#pragma once + +#ifndef NDK_WIDGETS_ABSTRACTTEXTAREAWIDGET_HPP +#define NDK_WIDGETS_ABSTRACTTEXTAREAWIDGET_HPP + +#include +#include +#include +#include +#include +#include + +namespace Ndk +{ + class NDK_API AbstractTextAreaWidget : public BaseWidget + { + public: + using CharacterFilter = std::function; + + AbstractTextAreaWidget(BaseWidget* parent); + AbstractTextAreaWidget(const AbstractTextAreaWidget&) = delete; + AbstractTextAreaWidget(AbstractTextAreaWidget&&) = default; + ~AbstractTextAreaWidget() = default; + + virtual void Clear(); + + //virtual TextAreaWidget* Clone() const = 0; + + void EnableLineWrap(bool enable = true); + inline void EnableMultiline(bool enable = true); + inline void EnableTabWriting(bool enable = true); + + inline void Erase(std::size_t glyphPosition); + virtual void Erase(std::size_t firstGlyph, std::size_t lastGlyph) = 0; + inline void EraseSelection(); + + inline const CharacterFilter& GetCharacterFilter() const; + inline const Nz::Vector2ui& GetCursorPosition() const; + inline Nz::Vector2ui GetCursorPosition(std::size_t glyphIndex) const; + inline EchoMode GetEchoMode() const; + inline std::size_t GetGlyphIndex() const; + inline std::size_t GetGlyphIndex(const Nz::Vector2ui& cursorPosition) const; + inline const Nz::String& GetText() const; + + Nz::Vector2ui GetHoveredGlyph(float x, float y) const; + + inline bool HasSelection() const; + + inline bool IsLineWrapEnabled() const; + inline bool IsMultilineEnabled() const; + inline bool IsReadOnly() const; + inline bool IsTabWritingEnabled() const; + + inline void MoveCursor(int offset); + inline void MoveCursor(const Nz::Vector2i& offset); + + inline Nz::Vector2ui NormalizeCursorPosition(Nz::Vector2ui cursorPosition) const; + + inline void SetCharacterFilter(CharacterFilter filter); + inline void SetCursorPosition(std::size_t glyphIndex); + 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 Write(const Nz::String& text); + inline void Write(const Nz::String& text, const Nz::Vector2ui& glyphPosition); + virtual void Write(const Nz::String& text, std::size_t glyphPosition) = 0; + + AbstractTextAreaWidget& operator=(const AbstractTextAreaWidget&) = delete; + AbstractTextAreaWidget& operator=(AbstractTextAreaWidget&&) = default; + + NazaraSignal(OnTextAreaCursorMove, const AbstractTextAreaWidget* /*textArea*/, Nz::Vector2ui* /*newCursorPosition*/); + NazaraSignal(OnTextAreaKeyBackspace, const AbstractTextAreaWidget* /*textArea*/, bool* /*ignoreDefaultAction*/); + NazaraSignal(OnTextAreaKeyDown, const AbstractTextAreaWidget* /*textArea*/, bool* /*ignoreDefaultAction*/); + NazaraSignal(OnTextAreaKeyEnd, const AbstractTextAreaWidget* /*textArea*/, bool* /*ignoreDefaultAction*/); + NazaraSignal(OnTextAreaKeyHome, const AbstractTextAreaWidget* /*textArea*/, bool* /*ignoreDefaultAction*/); + NazaraSignal(OnTextAreaKeyLeft, const AbstractTextAreaWidget* /*textArea*/, bool* /*ignoreDefaultAction*/); + NazaraSignal(OnTextAreaKeyReturn, const AbstractTextAreaWidget* /*textArea*/, bool* /*ignoreDefaultAction*/); + NazaraSignal(OnTextAreaKeyRight, const AbstractTextAreaWidget* /*textArea*/, bool* /*ignoreDefaultAction*/); + NazaraSignal(OnTextAreaKeyUp, const AbstractTextAreaWidget* /*textArea*/, bool* /*ignoreDefaultAction*/); + NazaraSignal(OnTextAreaSelection, const AbstractTextAreaWidget* /*textArea*/, Nz::Vector2ui* /*start*/, Nz::Vector2ui* /*end*/); + + protected: + virtual Nz::AbstractTextDrawer& GetTextDrawer() = 0; + virtual const Nz::AbstractTextDrawer& GetTextDrawer() const = 0; + + void Layout() override; + + virtual void HandleIndentation(bool add) = 0; + virtual void HandleSelectionIndentation(bool add) = 0; + virtual void HandleWordCursorMove(bool left) = 0; + + bool IsFocusable() const override; + void OnFocusLost() override; + void OnFocusReceived() override; + 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; + + inline void SetCursorPositionInternal(std::size_t glyphIndex); + inline void SetCursorPositionInternal(Nz::Vector2ui cursorPosition); + + void RefreshCursor(); + virtual void UpdateDisplayText() = 0; + void UpdateTextSprite(); + + CharacterFilter m_characterFilter; + EchoMode m_echoMode; + EntityHandle m_cursorEntity; + EntityHandle m_textEntity; + Nz::TextSpriteRef m_textSprite; + Nz::Vector2ui m_cursorPositionBegin; + Nz::Vector2ui m_cursorPositionEnd; + Nz::Vector2ui m_selectionCursor; + std::vector m_cursorSprites; + bool m_isLineWrapEnabled; + bool m_isMouseButtonDown; + bool m_multiLineEnabled; + bool m_readOnly; + bool m_tabEnabled; // writes (Shift+)Tab character if set to true + }; +} + +#include + +#endif // NDK_WIDGETS_ABSTRACTTEXTAREAWIDGET_HPP diff --git a/SDK/include/NDK/Widgets/AbstractTextAreaWidget.inl b/SDK/include/NDK/Widgets/AbstractTextAreaWidget.inl new file mode 100644 index 000000000..ca40d278b --- /dev/null +++ b/SDK/include/NDK/Widgets/AbstractTextAreaWidget.inl @@ -0,0 +1,252 @@ +// Copyright (C) 2017 Jérôme Leclercq +// This file is part of the "Nazara Development Kit" +// For conditions of distribution and use, see copyright notice in Prerequisites.hpp + +#include + +namespace Ndk +{ + inline void AbstractTextAreaWidget::EnableMultiline(bool enable) + { + m_multiLineEnabled = enable; + } + + inline void AbstractTextAreaWidget::EnableTabWriting(bool enable) + { + m_tabEnabled = enable; + } + + inline void AbstractTextAreaWidget::Erase(std::size_t glyphPosition) + { + Erase(glyphPosition, glyphPosition + 1U); + } + + inline void AbstractTextAreaWidget::EraseSelection() + { + if (!HasSelection()) + return; + + Erase(GetGlyphIndex(m_cursorPositionBegin), GetGlyphIndex(m_cursorPositionEnd)); + } + + inline const AbstractTextAreaWidget::CharacterFilter& AbstractTextAreaWidget::GetCharacterFilter() const + { + return m_characterFilter; + } + + inline const Nz::Vector2ui& AbstractTextAreaWidget::GetCursorPosition() const + { + return m_cursorPositionBegin; + } + + Nz::Vector2ui AbstractTextAreaWidget::GetCursorPosition(std::size_t glyphIndex) const + { + const Nz::AbstractTextDrawer& textDrawer = GetTextDrawer(); + + glyphIndex = std::min(glyphIndex, GetTextDrawer().GetGlyphCount()); + + std::size_t lineCount = textDrawer.GetLineCount(); + std::size_t line = 0U; + for (std::size_t i = line + 1; i < lineCount; ++i) + { + if (textDrawer.GetLine(i).glyphIndex > glyphIndex) + break; + + line = i; + } + + const auto& lineInfo = textDrawer.GetLine(line); + + Nz::Vector2ui cursorPos; + cursorPos.y = static_cast(line); + cursorPos.x = static_cast(glyphIndex - lineInfo.glyphIndex); + + return cursorPos; + } + + inline EchoMode AbstractTextAreaWidget::GetEchoMode() const + { + return m_echoMode; + } + + inline std::size_t AbstractTextAreaWidget::GetGlyphIndex() const + { + return GetGlyphIndex(m_cursorPositionBegin); + } + + inline std::size_t AbstractTextAreaWidget::GetGlyphIndex(const Nz::Vector2ui& cursorPosition) const + { + const Nz::AbstractTextDrawer& textDrawer = GetTextDrawer(); + + std::size_t glyphIndex = textDrawer.GetLine(cursorPosition.y).glyphIndex + cursorPosition.x; + if (textDrawer.GetLineCount() > cursorPosition.y + 1) + glyphIndex = std::min(glyphIndex, textDrawer.GetLine(cursorPosition.y + 1).glyphIndex - 1); + else + glyphIndex = std::min(glyphIndex, textDrawer.GetGlyphCount()); + + return glyphIndex; + } + + inline bool AbstractTextAreaWidget::HasSelection() const + { + return m_cursorPositionBegin != m_cursorPositionEnd; + } + + inline bool AbstractTextAreaWidget::IsLineWrapEnabled() const + { + return m_isLineWrapEnabled; + } + + inline bool AbstractTextAreaWidget::IsMultilineEnabled() const + { + return m_multiLineEnabled; + } + + inline bool AbstractTextAreaWidget::IsTabWritingEnabled() const + { + return m_tabEnabled; + } + + inline bool AbstractTextAreaWidget::IsReadOnly() const + { + return m_readOnly; + } + + inline void AbstractTextAreaWidget::MoveCursor(int offset) + { + std::size_t cursorGlyph = GetGlyphIndex(m_cursorPositionBegin); + if (offset >= 0) + SetCursorPosition(cursorGlyph + static_cast(offset)); + else + { + std::size_t nOffset = static_cast(-offset); + if (nOffset >= cursorGlyph) + SetCursorPosition(0); + else + SetCursorPosition(cursorGlyph - nOffset); + } + } + + inline void AbstractTextAreaWidget::MoveCursor(const Nz::Vector2i& offset) + { + auto ClampOffset = [] (unsigned int cursorPosition, int cursorOffset) -> unsigned int + { + if (cursorOffset >= 0) + return cursorPosition + cursorOffset; + else + { + unsigned int nOffset = static_cast(-cursorOffset); + if (nOffset >= cursorPosition) + return 0; + else + return cursorPosition - nOffset; + } + }; + + Nz::Vector2ui cursorPosition = m_cursorPositionBegin; + cursorPosition.x = ClampOffset(static_cast(cursorPosition.x), offset.x); + cursorPosition.y = ClampOffset(static_cast(cursorPosition.y), offset.y); + + SetCursorPosition(cursorPosition); + } + + inline Nz::Vector2ui AbstractTextAreaWidget::NormalizeCursorPosition(Nz::Vector2ui cursorPosition) const + { + const Nz::AbstractTextDrawer& textDrawer = GetTextDrawer(); + + std::size_t lineCount = textDrawer.GetLineCount(); + if (cursorPosition.y >= lineCount) + cursorPosition.y = static_cast(lineCount - 1); + + const auto& lineInfo = textDrawer.GetLine(cursorPosition.y); + if (cursorPosition.y + 1 < lineCount) + { + const auto& nextLineInfo = textDrawer.GetLine(cursorPosition.y + 1); + cursorPosition.x = std::min(cursorPosition.x, static_cast(nextLineInfo.glyphIndex - lineInfo.glyphIndex - 1)); + } + + return cursorPosition; + } + + inline void AbstractTextAreaWidget::SetCharacterFilter(CharacterFilter filter) + { + m_characterFilter = std::move(filter); + } + + inline void AbstractTextAreaWidget::SetCursorPosition(std::size_t glyphIndex) + { + Nz::Vector2ui position = GetCursorPosition(glyphIndex); + Nz::Vector2ui newPosition = position; + + OnTextAreaCursorMove(this, &newPosition); + + if (position == newPosition) + SetCursorPositionInternal(position); + else + SetCursorPositionInternal(GetGlyphIndex(newPosition)); + } + + inline void AbstractTextAreaWidget::SetCursorPosition(Nz::Vector2ui cursorPosition) + { + OnTextAreaCursorMove(this, &cursorPosition); + + return SetCursorPositionInternal(NormalizeCursorPosition(cursorPosition)); + } + + inline void AbstractTextAreaWidget::SetEchoMode(EchoMode echoMode) + { + m_echoMode = echoMode; + + UpdateDisplayText(); + } + + inline void AbstractTextAreaWidget::SetReadOnly(bool readOnly) + { + m_readOnly = readOnly; + m_cursorEntity->Enable(!m_readOnly && HasFocus()); + } + + inline void AbstractTextAreaWidget::SetSelection(Nz::Vector2ui fromPosition, Nz::Vector2ui toPosition) + { + // 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) + { + OnTextAreaSelection(this, &fromPosition, &toPosition); + + // Ensure begin is before end a second time (in case signal changed it) + if (toPosition.y < fromPosition.y || (toPosition.y == fromPosition.y && toPosition.x < fromPosition.x)) + std::swap(fromPosition, toPosition); + + m_cursorPositionBegin = NormalizeCursorPosition(fromPosition); + m_cursorPositionEnd = NormalizeCursorPosition(toPosition); + + RefreshCursor(); + } + } + + inline void AbstractTextAreaWidget::Write(const Nz::String& text) + { + Write(text, GetGlyphIndex(m_cursorPositionBegin)); + } + + inline void AbstractTextAreaWidget::Write(const Nz::String& text, const Nz::Vector2ui& glyphPosition) + { + Write(text, GetGlyphIndex(glyphPosition)); + } + + void AbstractTextAreaWidget::SetCursorPositionInternal(std::size_t glyphIndex) + { + return SetCursorPositionInternal(GetCursorPosition(glyphIndex)); + } + + inline void AbstractTextAreaWidget::SetCursorPositionInternal(Nz::Vector2ui cursorPosition) + { + m_cursorPositionBegin = cursorPosition; + m_cursorPositionEnd = m_cursorPositionBegin; + + RefreshCursor(); + } +} diff --git a/SDK/include/NDK/Widgets/RichTextAreaWidget.hpp b/SDK/include/NDK/Widgets/RichTextAreaWidget.hpp new file mode 100644 index 000000000..943bc2861 --- /dev/null +++ b/SDK/include/NDK/Widgets/RichTextAreaWidget.hpp @@ -0,0 +1,58 @@ +// Copyright (C) 2017 Jérôme Leclercq +// This file is part of the "Nazara Development Kit" +// For conditions of distribution and use, see copyright notice in Prerequisites.hpp + +#pragma once + +#ifndef NDK_WIDGETS_RICHTEXTAREAWIDGET_HPP +#define NDK_WIDGETS_RICHTEXTAREAWIDGET_HPP + +#include +#include + +namespace Ndk +{ + class NDK_API RichTextAreaWidget : public AbstractTextAreaWidget + { + public: + RichTextAreaWidget(BaseWidget* parent); + RichTextAreaWidget(const RichTextAreaWidget&) = delete; + RichTextAreaWidget(RichTextAreaWidget&&) = default; + ~RichTextAreaWidget() = default; + + void AppendText(const Nz::String& text); + + void Clear() override; + + void Erase(std::size_t firstGlyph, std::size_t lastGlyph) override; + + inline unsigned int GetCharacterSize() const; + inline const Nz::Color& GetTextColor() const; + inline Nz::Font* GetTextFont() const; + + inline void SetCharacterSize(unsigned int characterSize); + inline void SetTextColor(const Nz::Color& color); + inline void SetTextFont(Nz::FontRef font); + + void Write(const Nz::String& text, std::size_t glyphPosition) override; + + RichTextAreaWidget& operator=(const RichTextAreaWidget&) = delete; + RichTextAreaWidget& operator=(RichTextAreaWidget&&) = default; + + private: + Nz::AbstractTextDrawer& GetTextDrawer() override; + const Nz::AbstractTextDrawer& GetTextDrawer() const override; + + void HandleIndentation(bool add) override; + void HandleSelectionIndentation(bool add) override; + void HandleWordCursorMove(bool left) override; + + void UpdateDisplayText(); + + Nz::RichTextDrawer m_drawer; + }; +} + +#include + +#endif // NDK_WIDGETS_TEXTAREAWIDGET_HPP diff --git a/SDK/include/NDK/Widgets/RichTextAreaWidget.inl b/SDK/include/NDK/Widgets/RichTextAreaWidget.inl new file mode 100644 index 000000000..2c31af9e5 --- /dev/null +++ b/SDK/include/NDK/Widgets/RichTextAreaWidget.inl @@ -0,0 +1,38 @@ +// Copyright (C) 2017 Jérôme Leclercq +// This file is part of the "Nazara Development Kit" +// For conditions of distribution and use, see copyright notice in Prerequisites.hpp + +#include + +namespace Ndk +{ + inline unsigned int RichTextAreaWidget::GetCharacterSize() const + { + return m_drawer.GetDefaultCharacterSize(); + } + + inline const Nz::Color& RichTextAreaWidget::GetTextColor() const + { + return m_drawer.GetDefaultColor(); + } + + inline Nz::Font* RichTextAreaWidget::GetTextFont() const + { + return m_drawer.GetDefaultFont(); + } + + inline void RichTextAreaWidget::SetCharacterSize(unsigned int characterSize) + { + m_drawer.SetDefaultCharacterSize(characterSize); + } + + inline void RichTextAreaWidget::SetTextColor(const Nz::Color& color) + { + m_drawer.SetDefaultColor(color); + } + + inline void RichTextAreaWidget::SetTextFont(Nz::FontRef font) + { + m_drawer.SetDefaultFont(std::move(font)); + } +} diff --git a/SDK/include/NDK/Widgets/TextAreaWidget.hpp b/SDK/include/NDK/Widgets/TextAreaWidget.hpp index ebce9e464..f28de9e57 100644 --- a/SDK/include/NDK/Widgets/TextAreaWidget.hpp +++ b/SDK/include/NDK/Widgets/TextAreaWidget.hpp @@ -7,20 +7,14 @@ #ifndef NDK_WIDGETS_TEXTAREAWIDGET_HPP #define NDK_WIDGETS_TEXTAREAWIDGET_HPP -#include #include -#include -#include -#include -#include +#include namespace Ndk { - class NDK_API TextAreaWidget : public BaseWidget + class NDK_API TextAreaWidget : public AbstractTextAreaWidget { public: - using CharacterFilter = std::function; - TextAreaWidget(BaseWidget* parent); TextAreaWidget(const TextAreaWidget&) = delete; TextAreaWidget(TextAreaWidget&&) = default; @@ -28,115 +22,46 @@ namespace Ndk void AppendText(const Nz::String& text); - inline void Clear(); + void Clear() override; - //virtual TextAreaWidget* Clone() const = 0; + using AbstractTextAreaWidget::Erase; + void Erase(std::size_t firstGlyph, std::size_t lastGlyph) override; - void EnableLineWrap(bool enable = true); - inline void EnableMultiline(bool enable = true); - inline void EnableTabWriting(bool enable = true); - - inline void Erase(std::size_t glyphPosition); - void Erase(std::size_t firstGlyph, std::size_t lastGlyph); - void EraseSelection(); - - inline const CharacterFilter& GetCharacterFilter() const; 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; - inline std::size_t GetGlyphIndex(const Nz::Vector2ui& cursorPosition) const; inline const Nz::String& GetText() const; inline const Nz::Color& GetTextColor() const; inline Nz::Font* GetTextFont() const; inline const Nz::Color& GetTextOulineColor() const; inline float GetTextOulineThickness() const; - Nz::Vector2ui GetHoveredGlyph(float x, float y) const; - - inline bool HasSelection() const; - - inline bool IsLineWrapEnabled() const; - inline bool IsMultilineEnabled() const; - inline bool IsReadOnly() const; - inline bool IsTabWritingEnabled() const; - - inline void MoveCursor(int offset); - inline void MoveCursor(const Nz::Vector2i& offset); - - inline Nz::Vector2ui NormalizeCursorPosition(Nz::Vector2ui cursorPosition) const; - - inline void SetCharacterFilter(CharacterFilter filter); void SetCharacterSize(unsigned int characterSize); - inline void SetCursorPosition(std::size_t glyphIndex); - 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); inline void SetTextFont(Nz::FontRef font); inline void SetTextOutlineColor(const Nz::Color& color); inline void SetTextOutlineThickness(float thickness); - inline void Write(const Nz::String& text); - inline void Write(const Nz::String& text, const Nz::Vector2ui& glyphPosition); - void Write(const Nz::String& text, std::size_t glyphPosition); + using AbstractTextAreaWidget::Write; + void Write(const Nz::String& text, std::size_t glyphPosition) override; TextAreaWidget& operator=(const TextAreaWidget&) = delete; TextAreaWidget& operator=(TextAreaWidget&&) = default; - NazaraSignal(OnTextAreaCursorMove, const TextAreaWidget* /*textArea*/, Nz::Vector2ui* /*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*/); - NazaraSignal(OnTextAreaKeyUp, const TextAreaWidget* /*textArea*/, bool* /*ignoreDefaultAction*/); - NazaraSignal(OnTextAreaSelection, const TextAreaWidget* /*textArea*/, Nz::Vector2ui* /*start*/, Nz::Vector2ui* /*end*/); - NazaraSignal(OnTextChanged, const TextAreaWidget* /*textArea*/, const Nz::String& /*text*/); + NazaraSignal(OnTextChanged, const AbstractTextAreaWidget* /*textArea*/, const Nz::String& /*text*/); private: - void Layout() override; + Nz::AbstractTextDrawer& GetTextDrawer() override; + const Nz::AbstractTextDrawer& GetTextDrawer() const override; - bool IsFocusable() const override; - void OnFocusLost() override; - void OnFocusReceived() override; - 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 HandleIndentation(bool add) override; + void HandleSelectionIndentation(bool add) override; + void HandleWordCursorMove(bool left) override; - inline void SetCursorPositionInternal(std::size_t glyphIndex); - inline void SetCursorPositionInternal(Nz::Vector2ui cursorPosition); - - void RefreshCursor(); void UpdateDisplayText(); - void UpdateTextSprite(); - CharacterFilter m_characterFilter; - EchoMode m_echoMode; - EntityHandle m_cursorEntity; - EntityHandle m_textEntity; Nz::SimpleTextDrawer m_drawer; Nz::String m_text; - Nz::TextSpriteRef m_textSprite; - Nz::Vector2ui m_cursorPositionBegin; - Nz::Vector2ui m_cursorPositionEnd; - Nz::Vector2ui m_selectionCursor; - std::vector m_cursorSprites; - bool m_isLineWrapEnabled; - bool m_isMouseButtonDown; - bool m_multiLineEnabled; - bool m_readOnly; - bool m_tabEnabled; // writes (Shift+)Tab character if set to true }; } diff --git a/SDK/include/NDK/Widgets/TextAreaWidget.inl b/SDK/include/NDK/Widgets/TextAreaWidget.inl index bdb95854e..aeb7f6914 100644 --- a/SDK/include/NDK/Widgets/TextAreaWidget.inl +++ b/SDK/include/NDK/Widgets/TextAreaWidget.inl @@ -6,98 +6,16 @@ namespace Ndk { - inline void TextAreaWidget::Clear() - { - m_cursorPositionBegin.MakeZero(); - m_cursorPositionEnd.MakeZero(); - m_drawer.Clear(); - m_text.Clear(); - m_textSprite->Update(m_drawer); - SetPreferredSize(Nz::Vector2f(m_textSprite->GetBoundingVolume().obb.localBox.GetLengths())); - - RefreshCursor(); - OnTextChanged(this, m_text); - } - - inline void TextAreaWidget::EnableMultiline(bool enable) - { - m_multiLineEnabled = enable; - } - - inline void TextAreaWidget::EnableTabWriting(bool enable) - { - m_tabEnabled = enable; - } - - inline void TextAreaWidget::Erase(std::size_t glyphPosition) - { - Erase(glyphPosition, glyphPosition + 1U); - } - - inline const TextAreaWidget::CharacterFilter& TextAreaWidget::GetCharacterFilter() const - { - return m_characterFilter; - } - inline unsigned int TextAreaWidget::GetCharacterSize() const { return m_drawer.GetCharacterSize(); } - inline const Nz::Vector2ui& TextAreaWidget::GetCursorPosition() const - { - 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 { return m_drawer.GetText(); } - inline EchoMode TextAreaWidget::GetEchoMode() const - { - return m_echoMode; - } - - inline std::size_t TextAreaWidget::GetGlyphIndex() const - { - return GetGlyphIndex(m_cursorPositionBegin); - } - - inline std::size_t TextAreaWidget::GetGlyphIndex(const Nz::Vector2ui& cursorPosition) const - { - std::size_t glyphIndex = m_drawer.GetLine(cursorPosition.y).glyphIndex + cursorPosition.x; - if (m_drawer.GetLineCount() > cursorPosition.y + 1) - glyphIndex = std::min(glyphIndex, m_drawer.GetLine(cursorPosition.y + 1).glyphIndex - 1); - else - glyphIndex = std::min(glyphIndex, m_drawer.GetGlyphCount()); - - return glyphIndex; - } - inline const Nz::String& TextAreaWidget::GetText() const { return m_text; @@ -123,144 +41,6 @@ namespace Ndk return m_drawer.GetOutlineThickness(); } - inline bool TextAreaWidget::HasSelection() const - { - return m_cursorPositionBegin != m_cursorPositionEnd; - } - - inline bool TextAreaWidget::IsLineWrapEnabled() const - { - return m_isLineWrapEnabled; - } - - inline bool TextAreaWidget::IsMultilineEnabled() const - { - return m_multiLineEnabled; - } - - inline bool TextAreaWidget::IsTabWritingEnabled() const - { - return m_tabEnabled; - } - - inline bool TextAreaWidget::IsReadOnly() const - { - return m_readOnly; - } - - inline void TextAreaWidget::MoveCursor(int offset) - { - std::size_t cursorGlyph = GetGlyphIndex(m_cursorPositionBegin); - if (offset >= 0) - SetCursorPosition(cursorGlyph + static_cast(offset)); - else - { - std::size_t nOffset = static_cast(-offset); - if (nOffset >= cursorGlyph) - SetCursorPosition(0); - else - SetCursorPosition(cursorGlyph - nOffset); - } - } - - inline void TextAreaWidget::MoveCursor(const Nz::Vector2i& offset) - { - auto ClampOffset = [] (unsigned int cursorPosition, int cursorOffset) -> unsigned int - { - if (cursorOffset >= 0) - return cursorPosition + cursorOffset; - else - { - unsigned int nOffset = static_cast(-cursorOffset); - if (nOffset >= cursorPosition) - return 0; - else - return cursorPosition - nOffset; - } - }; - - Nz::Vector2ui cursorPosition = m_cursorPositionBegin; - cursorPosition.x = ClampOffset(static_cast(cursorPosition.x), offset.x); - cursorPosition.y = ClampOffset(static_cast(cursorPosition.y), offset.y); - - SetCursorPosition(cursorPosition); - } - - inline Nz::Vector2ui TextAreaWidget::NormalizeCursorPosition(Nz::Vector2ui cursorPosition) const - { - std::size_t lineCount = m_drawer.GetLineCount(); - if (cursorPosition.y >= lineCount) - cursorPosition.y = static_cast(lineCount - 1); - - const auto& lineInfo = m_drawer.GetLine(cursorPosition.y); - if (cursorPosition.y + 1 < lineCount) - { - const auto& nextLineInfo = m_drawer.GetLine(cursorPosition.y + 1); - cursorPosition.x = std::min(cursorPosition.x, static_cast(nextLineInfo.glyphIndex - lineInfo.glyphIndex - 1)); - } - - return cursorPosition; - } - - inline void TextAreaWidget::SetCharacterFilter(CharacterFilter filter) - { - m_characterFilter = std::move(filter); - } - - inline void TextAreaWidget::SetCursorPosition(std::size_t glyphIndex) - { - Nz::Vector2ui position = GetCursorPosition(glyphIndex); - Nz::Vector2ui newPosition = position; - - OnTextAreaCursorMove(this, &newPosition); - - if (position == newPosition) - SetCursorPositionInternal(position); - else - SetCursorPositionInternal(GetGlyphIndex(newPosition)); - } - - inline void TextAreaWidget::SetCursorPosition(Nz::Vector2ui cursorPosition) - { - OnTextAreaCursorMove(this, &cursorPosition); - - return SetCursorPositionInternal(NormalizeCursorPosition(cursorPosition)); - } - - inline void TextAreaWidget::SetEchoMode(EchoMode echoMode) - { - m_echoMode = echoMode; - - UpdateDisplayText(); - } - - inline void TextAreaWidget::SetReadOnly(bool readOnly) - { - m_readOnly = readOnly; - m_cursorEntity->Enable(!m_readOnly && HasFocus()); - } - - inline void TextAreaWidget::SetSelection(Nz::Vector2ui fromPosition, Nz::Vector2ui toPosition) - { - // 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) - { - OnTextAreaSelection(this, &fromPosition, &toPosition); - - // Ensure begin is before end a second time (in case signal changed it) - if (toPosition.y < fromPosition.y || (toPosition.y == fromPosition.y && toPosition.x < fromPosition.x)) - std::swap(fromPosition, toPosition); - - m_cursorPositionBegin = NormalizeCursorPosition(fromPosition); - m_cursorPositionEnd = NormalizeCursorPosition(toPosition); - - RefreshCursor(); - } - } - inline void TextAreaWidget::SetText(const Nz::String& text) { m_text = text; @@ -296,27 +76,4 @@ namespace Ndk UpdateDisplayText(); } - - inline void TextAreaWidget::Write(const Nz::String& text) - { - Write(text, GetGlyphIndex(m_cursorPositionBegin)); - } - - inline void TextAreaWidget::Write(const Nz::String& text, const Nz::Vector2ui& glyphPosition) - { - Write(text, GetGlyphIndex(glyphPosition)); - } - - void TextAreaWidget::SetCursorPositionInternal(std::size_t glyphIndex) - { - return SetCursorPositionInternal(GetCursorPosition(glyphIndex)); - } - - inline void TextAreaWidget::SetCursorPositionInternal(Nz::Vector2ui cursorPosition) - { - m_cursorPositionBegin = cursorPosition; - m_cursorPositionEnd = m_cursorPositionBegin; - - RefreshCursor(); - } } diff --git a/SDK/src/NDK/Console.cpp b/SDK/src/NDK/Console.cpp index abd333bd3..292ea992c 100644 --- a/SDK/src/NDK/Console.cpp +++ b/SDK/src/NDK/Console.cpp @@ -61,25 +61,25 @@ namespace Ndk // Protect input prefix from erasure/selection m_input->SetCursorPosition(s_inputPrefixSize); - m_input->OnTextAreaCursorMove.Connect([](const TextAreaWidget* textArea, Nz::Vector2ui* newCursorPos) + m_input->OnTextAreaCursorMove.Connect([](const AbstractTextAreaWidget* textArea, Nz::Vector2ui* newCursorPos) { newCursorPos->x = std::max(newCursorPos->x, static_cast(s_inputPrefixSize)); }); - m_input->OnTextAreaSelection.Connect([](const TextAreaWidget* textArea, Nz::Vector2ui* start, Nz::Vector2ui* end) + m_input->OnTextAreaSelection.Connect([](const AbstractTextAreaWidget* textArea, Nz::Vector2ui* start, Nz::Vector2ui* end) { start->x = std::max(start->x, static_cast(s_inputPrefixSize)); end->x = std::max(end->x, static_cast(s_inputPrefixSize)); }); - m_input->OnTextAreaKeyBackspace.Connect([](const TextAreaWidget* textArea, bool* ignoreDefaultAction) + m_input->OnTextAreaKeyBackspace.Connect([](const AbstractTextAreaWidget* textArea, bool* ignoreDefaultAction) { if (textArea->GetGlyphIndex() <= s_inputPrefixSize) *ignoreDefaultAction = true; }); // Handle history - m_input->OnTextAreaKeyUp.Connect([&] (const TextAreaWidget* textArea, bool* ignoreDefaultAction) + m_input->OnTextAreaKeyUp.Connect([&] (const AbstractTextAreaWidget* textArea, bool* ignoreDefaultAction) { *ignoreDefaultAction = true; @@ -89,7 +89,7 @@ namespace Ndk m_input->SetText(s_inputPrefix + m_commandHistory[m_historyPosition]); }); - m_input->OnTextAreaKeyDown.Connect([&] (const TextAreaWidget* textArea, bool* ignoreDefaultAction) + m_input->OnTextAreaKeyDown.Connect([&] (const AbstractTextAreaWidget* textArea, bool* ignoreDefaultAction) { *ignoreDefaultAction = true; @@ -187,7 +187,7 @@ namespace Ndk /*! * \brief Performs this action when an input is added to the console */ - void Console::ExecuteInput(const TextAreaWidget* textArea, bool* ignoreDefaultAction) + void Console::ExecuteInput(const AbstractTextAreaWidget* textArea, bool* ignoreDefaultAction) { NazaraAssert(textArea == m_input, "Unexpected signal from an other text area"); diff --git a/SDK/src/NDK/Widgets/AbstractTextAreaWidget.cpp b/SDK/src/NDK/Widgets/AbstractTextAreaWidget.cpp new file mode 100644 index 000000000..ddb3467f5 --- /dev/null +++ b/SDK/src/NDK/Widgets/AbstractTextAreaWidget.cpp @@ -0,0 +1,489 @@ +// Copyright (C) 2017 Jérôme Leclercq +// This file is part of the "Nazara Development Kit" +// For conditions of distribution and use, see copyright notice in Prerequisites.hpp + +#include +#include +#include +#include +#include + +namespace Ndk +{ + namespace + { + constexpr float paddingWidth = 5.f; + constexpr float paddingHeight = 3.f; + } + + AbstractTextAreaWidget::AbstractTextAreaWidget(BaseWidget* parent) : + BaseWidget(parent), + m_characterFilter(), + m_echoMode(EchoMode_Normal), + m_cursorPositionBegin(0U, 0U), + m_cursorPositionEnd(0U, 0U), + m_isLineWrapEnabled(false), + m_isMouseButtonDown(false), + m_multiLineEnabled(false), + m_readOnly(false), + m_tabEnabled(false) + { + m_textSprite = Nz::TextSprite::New(); + + m_textEntity = CreateEntity(); + m_textEntity->AddComponent().Attach(m_textSprite); + + auto& textNode = m_textEntity->AddComponent(); + textNode.SetParent(this); + textNode.SetPosition(paddingWidth, paddingHeight); + + m_cursorEntity = CreateEntity(); + m_cursorEntity->AddComponent(); + m_cursorEntity->AddComponent().SetParent(m_textEntity); + m_cursorEntity->GetComponent(); + m_cursorEntity->Enable(false); + + SetCursor(Nz::SystemCursor_Text); + + EnableBackground(true); + } + + void AbstractTextAreaWidget::Clear() + { + Nz::AbstractTextDrawer& textDrawer = GetTextDrawer(); + + m_cursorPositionBegin.MakeZero(); + m_cursorPositionEnd.MakeZero(); + textDrawer.Clear(); + m_textSprite->Update(textDrawer); + SetPreferredSize(Nz::Vector2f(m_textSprite->GetBoundingVolume().obb.localBox.GetLengths())); + + RefreshCursor(); + } + + void AbstractTextAreaWidget::EnableLineWrap(bool enable) + { + if (m_isLineWrapEnabled != enable) + { + m_isLineWrapEnabled = enable; + + Nz::AbstractTextDrawer& textDrawer = GetTextDrawer(); + + if (enable) + textDrawer.SetMaxLineWidth(GetWidth()); + else + textDrawer.SetMaxLineWidth(std::numeric_limits::infinity()); + + UpdateTextSprite(); + } + } + + Nz::Vector2ui AbstractTextAreaWidget::GetHoveredGlyph(float x, float y) const + { + const Nz::AbstractTextDrawer& textDrawer = GetTextDrawer(); + + auto& textNode = m_textEntity->GetComponent(); + Nz::Vector2f textPosition = Nz::Vector2f(textNode.GetPosition(Nz::CoordSys_Local)); + x -= textPosition.x; + y -= textPosition.y; + + std::size_t glyphCount = textDrawer.GetGlyphCount(); + if (glyphCount > 0) + { + std::size_t lineCount = textDrawer.GetLineCount(); + std::size_t line = 0U; + for (; line < lineCount - 1; ++line) + { + Nz::Rectf lineBounds = textDrawer.GetLine(line).bounds; + if (lineBounds.GetMaximum().y > y) + break; + } + + std::size_t upperLimit = (line != lineCount - 1) ? textDrawer.GetLine(line + 1).glyphIndex : glyphCount + 1; + + std::size_t firstLineGlyph = textDrawer.GetLine(line).glyphIndex; + std::size_t i = firstLineGlyph; + for (; i < upperLimit - 1; ++i) + { + Nz::Rectf bounds = textDrawer.GetGlyph(i).bounds; + if (x < bounds.x + bounds.width * 0.75f) + break; + } + + return Nz::Vector2ui(Nz::Vector2(i - firstLineGlyph, line)); + } + + return Nz::Vector2ui::Zero(); + } + + void AbstractTextAreaWidget::Layout() + { + BaseWidget::Layout(); + + if (m_isLineWrapEnabled) + { + Nz::AbstractTextDrawer& textDrawer = GetTextDrawer(); + + textDrawer.SetMaxLineWidth(GetWidth()); + UpdateTextSprite(); + } + + RefreshCursor(); + } + + bool AbstractTextAreaWidget::IsFocusable() const + { + return !m_readOnly; + } + + void AbstractTextAreaWidget::OnFocusLost() + { + m_cursorEntity->Disable(); + } + + void AbstractTextAreaWidget::OnFocusReceived() + { + if (!m_readOnly) + m_cursorEntity->Enable(true); + } + + bool AbstractTextAreaWidget::OnKeyPressed(const Nz::WindowEvent::KeyEvent& key) + { + const Nz::AbstractTextDrawer& textDrawer = GetTextDrawer(); + + switch (key.code) + { + case Nz::Keyboard::Backspace: + { + bool ignoreDefaultAction = false; + OnTextAreaKeyBackspace(this, &ignoreDefaultAction); + + std::size_t cursorGlyphEnd = GetGlyphIndex(m_cursorPositionEnd); + + if (ignoreDefaultAction || cursorGlyphEnd == 0) + return true; + + // When a text is selected, delete key does the same as delete and leave the character behind it + if (HasSelection()) + EraseSelection(); + else + { + MoveCursor(-1); + Erase(GetGlyphIndex(m_cursorPositionBegin)); + } + + return true; + } + + case Nz::Keyboard::Delete: + { + if (HasSelection()) + EraseSelection(); + else + Erase(GetGlyphIndex(m_cursorPositionBegin)); + + return true; + } + + case Nz::Keyboard::Down: + { + bool ignoreDefaultAction = false; + OnTextAreaKeyDown(this, &ignoreDefaultAction); + + 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; + + std::size_t lineCount = textDrawer.GetLineCount(); + if (key.control && lineCount > 0) + SetCursorPosition({ static_cast(textDrawer.GetLineGlyphCount(lineCount - 1)), static_cast(lineCount - 1) }); + else + SetCursorPosition({ static_cast(textDrawer.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, key.control ? 0U : m_cursorPositionEnd.y }); + return true; + } + + case Nz::Keyboard::Left: + { + bool ignoreDefaultAction = false; + OnTextAreaKeyLeft(this, &ignoreDefaultAction); + + if (ignoreDefaultAction) + return true; + + if (HasSelection()) + SetCursorPosition(m_cursorPositionBegin); + else if (key.control) + HandleWordCursorMove(true); + else + MoveCursor(-1); + + return true; + } + + case Nz::Keyboard::Return: + { + bool ignoreDefaultAction = false; + OnTextAreaKeyReturn(this, &ignoreDefaultAction); + + if (ignoreDefaultAction) + return true; + + if (!m_multiLineEnabled) + break; + + if (HasSelection()) + EraseSelection(); + + Write(Nz::String('\n')); + return true;; + } + + case Nz::Keyboard::Right: + { + bool ignoreDefaultAction = false; + OnTextAreaKeyRight(this, &ignoreDefaultAction); + + if (ignoreDefaultAction) + return true; + + if (HasSelection()) + SetCursorPosition(m_cursorPositionEnd); + else if (key.control) + HandleWordCursorMove(false); + else + MoveCursor(1); + + return true; + } + + case Nz::Keyboard::Up: + { + bool ignoreDefaultAction = false; + OnTextAreaKeyUp(this, &ignoreDefaultAction); + + if (ignoreDefaultAction) + return true; + + if (HasSelection()) + SetCursorPosition(m_cursorPositionBegin); + + MoveCursor({0, -1}); + return true; + } + + case Nz::Keyboard::Tab: + { + if (!m_tabEnabled) + return false; + + if (HasSelection()) + HandleSelectionIndentation(!key.shift); + else + HandleIndentation(!key.shift); + + return true; + } + + default: + break; + } + + return false; + } + + void AbstractTextAreaWidget::OnKeyReleased(const Nz::WindowEvent::KeyEvent& /*key*/) + { + } + + void AbstractTextAreaWidget::OnMouseButtonPress(int x, int y, Nz::Mouse::Button button) + { + if (button == Nz::Mouse::Left) + { + SetFocus(); + + Nz::Vector2ui hoveredGlyph = GetHoveredGlyph(float(x), float(y)); + + // 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 AbstractTextAreaWidget::OnMouseButtonRelease(int, int, Nz::Mouse::Button button) + { + if (button == Nz::Mouse::Left) + m_isMouseButtonDown = false; + } + + void AbstractTextAreaWidget::OnMouseEnter() + { + if (!Nz::Mouse::IsButtonPressed(Nz::Mouse::Left)) + m_isMouseButtonDown = false; + } + + void AbstractTextAreaWidget::OnMouseMoved(int x, int y, int deltaX, int deltaY) + { + if (m_isMouseButtonDown) + SetSelection(m_selectionCursor, GetHoveredGlyph(float(x), float(y))); + } + + void AbstractTextAreaWidget::OnTextEntered(char32_t character, bool /*repeated*/) + { + if (m_readOnly) + return; + + if (Nz::Unicode::GetCategory(character) == Nz::Unicode::Category_Other_Control || (m_characterFilter && !m_characterFilter(character))) + return; + + if (HasSelection()) + EraseSelection(); + + Write(Nz::String::Unicode(character)); + } + + void AbstractTextAreaWidget::RefreshCursor() + { + if (m_readOnly) + return; + + const Nz::AbstractTextDrawer& textDrawer = GetTextDrawer(); + + auto GetGlyph = [&](const Nz::Vector2ui& glyphPosition, std::size_t* glyphIndex) -> const Nz::AbstractTextDrawer::Glyph* + { + if (glyphPosition.y >= textDrawer.GetLineCount()) + return nullptr; + + const auto& lineInfo = textDrawer.GetLine(glyphPosition.y); + + std::size_t cursorGlyph = GetGlyphIndex({ glyphPosition.x, glyphPosition.y }); + if (glyphIndex) + *glyphIndex = cursorGlyph; + + std::size_t glyphCount = textDrawer.GetGlyphCount(); + if (glyphCount > 0 && lineInfo.glyphIndex < cursorGlyph) + { + const auto& glyph = textDrawer.GetGlyph(std::min(cursorGlyph, glyphCount - 1)); + return &glyph; + } + else + return nullptr; + }; + + // Move text so that cursor is always visible + const auto* lastGlyph = GetGlyph(m_cursorPositionEnd, nullptr); + float glyphPos = (lastGlyph) ? lastGlyph->bounds.x : 0.f; + float glyphWidth = (lastGlyph) ? lastGlyph->bounds.width : 0.f; + + auto& node = m_textEntity->GetComponent(); + float textPosition = node.GetPosition(Nz::CoordSys_Local).x - paddingWidth; + float cursorPosition = glyphPos + textPosition; + float width = GetWidth(); + + if (width <= textDrawer.GetBounds().width) + { + if (cursorPosition + glyphWidth > width) + node.Move(width - cursorPosition - glyphWidth, 0.f); + else if (cursorPosition - glyphWidth < 0.f) + node.Move(-cursorPosition + glyphWidth, 0.f); + } + else + node.Move(-textPosition, 0.f); // Reset text position if we have enough room to show everything + + // Show cursor/selection + std::size_t selectionLineCount = m_cursorPositionEnd.y - m_cursorPositionBegin.y + 1; + std::size_t oldSpriteCount = m_cursorSprites.size(); + if (m_cursorSprites.size() != selectionLineCount) + { + 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")); + } + } + + GraphicsComponent& gfxComponent = m_cursorEntity->GetComponent(); + gfxComponent.Clear(); + + for (unsigned int i = m_cursorPositionBegin.y; i <= m_cursorPositionEnd.y; ++i) + { + const auto& lineInfo = textDrawer.GetLine(i); + + Nz::SpriteRef& cursorSprite = m_cursorSprites[i - m_cursorPositionBegin.y]; + if (i == m_cursorPositionBegin.y || i == m_cursorPositionEnd.y) + { + auto GetGlyphPos = [&](const Nz::Vector2ui& glyphPosition) + { + std::size_t glyphIndex; + const auto* glyph = GetGlyph(glyphPosition, &glyphIndex); + if (glyph) + { + float position = glyph->bounds.x; + if (glyphIndex >= textDrawer.GetGlyphCount()) + position += glyph->bounds.width; + + return position; + } + else + return 0.f; + }; + + float beginX = (i == m_cursorPositionBegin.y) ? GetGlyphPos({ m_cursorPositionBegin.x, i }) : 0.f; + float endX = (i == m_cursorPositionEnd.y) ? GetGlyphPos({ m_cursorPositionEnd.x, i }) : 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, lineInfo.bounds.height); + + 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, lineInfo.bounds.height); + + gfxComponent.Attach(cursorSprite, Nz::Matrix4f::Translate({ 0.f, lineInfo.bounds.y, 0.f })); + } + } + } + + void AbstractTextAreaWidget::UpdateTextSprite() + { + m_textSprite->Update(GetTextDrawer()); + SetPreferredSize(Nz::Vector2f(m_textSprite->GetBoundingVolume().obb.localBox.GetLengths())); + } +} diff --git a/SDK/src/NDK/Widgets/RichTextAreaWidget.cpp b/SDK/src/NDK/Widgets/RichTextAreaWidget.cpp new file mode 100644 index 000000000..a80c0e29d --- /dev/null +++ b/SDK/src/NDK/Widgets/RichTextAreaWidget.cpp @@ -0,0 +1,156 @@ +// Copyright (C) 2017 Jérôme Leclercq +// This file is part of the "Nazara Development Kit" +// For conditions of distribution and use, see copyright notice in Prerequisites.hpp + +#include + +namespace Ndk +{ + RichTextAreaWidget::RichTextAreaWidget(BaseWidget* parent) : + AbstractTextAreaWidget(parent) + { + Layout(); + } + + void RichTextAreaWidget::AppendText(const Nz::String& text) + { + //m_text += text; + switch (m_echoMode) + { + case EchoMode_Normal: + m_drawer.AppendText(text); + break; + + case EchoMode_Password: + m_drawer.AppendText(Nz::String(text.GetLength(), '*')); + break; + + case EchoMode_PasswordExceptLast: + { + /*m_drawer.Clear(); + std::size_t textLength = m_text.GetLength(); + if (textLength >= 2) + { + std::size_t lastCharacterPosition = m_text.GetCharacterPosition(textLength - 2); + if (lastCharacterPosition != Nz::String::npos) + m_drawer.AppendText(Nz::String(textLength - 1, '*')); + } + + if (textLength >= 1) + m_drawer.AppendText(m_text.SubString(m_text.GetCharacterPosition(textLength - 1)));*/ + + break; + } + } + + UpdateTextSprite(); + + //OnTextChanged(this, m_text); + } + + void RichTextAreaWidget::Clear() + { + AbstractTextAreaWidget::Clear(); + } + + void RichTextAreaWidget::Erase(std::size_t firstGlyph, std::size_t lastGlyph) + { + if (firstGlyph > lastGlyph) + std::swap(firstGlyph, lastGlyph); + + std::size_t textLength = m_drawer.GetGlyphCount(); + if (firstGlyph > textLength) + return; + + std::size_t firstBlock = m_drawer.FindBlock(firstGlyph); + std::size_t lastBlock = m_drawer.FindBlock(lastGlyph); + if (firstBlock == lastBlock) + { + const Nz::String& blockText = m_drawer.GetBlockText(firstBlock); + std::size_t blockFirstGlyph = m_drawer.GetBlockFirstGlyphIndex(firstBlock); + + Nz::String newText; + if (firstGlyph > blockFirstGlyph) + { + std::size_t characterPosition = blockText.GetCharacterPosition(firstGlyph - blockFirstGlyph - 1); + NazaraAssert(characterPosition != Nz::String::npos, "Invalid character position"); + + newText.Append(blockText.SubString(0, characterPosition)); + } + + if (lastGlyph < textLength) + { + std::size_t characterPosition = blockText.GetCharacterPosition(lastGlyph - blockFirstGlyph); + NazaraAssert(characterPosition != Nz::String::npos, "Invalid character position"); + + newText.Append(blockText.SubString(characterPosition)); + } + + m_drawer.SetBlockText(firstBlock, newText); + } + else + { + // More complicated algorithm, yay + } + + UpdateDisplayText(); + } + + void RichTextAreaWidget::Write(const Nz::String& text, std::size_t glyphPosition) + { + auto block = m_drawer.GetBlock(m_drawer.FindBlock(glyphPosition)); + std::size_t firstGlyph = block.GetFirstGlyphIndex(); + assert(glyphPosition >= firstGlyph); + + Nz::String blockText = block.GetText(); + std::size_t characterPosition = blockText.GetCharacterPosition(glyphPosition - firstGlyph); + blockText.Insert(characterPosition, text); + + block.SetText(blockText); + SetCursorPosition(glyphPosition + text.GetLength()); + + UpdateDisplayText(); + } + + Nz::AbstractTextDrawer& RichTextAreaWidget::GetTextDrawer() + { + return m_drawer; + } + + const Nz::AbstractTextDrawer& RichTextAreaWidget::GetTextDrawer() const + { + return m_drawer; + } + + void RichTextAreaWidget::HandleIndentation(bool add) + { + } + + void RichTextAreaWidget::HandleSelectionIndentation(bool add) + { + } + + void RichTextAreaWidget::HandleWordCursorMove(bool left) + { + } + + void RichTextAreaWidget::UpdateDisplayText() + { + /*m_drawer.Clear(); + switch (m_echoMode) + { + case EchoMode_Normal: + m_drawer.AppendText(m_text); + break; + + case EchoMode_Password: + case EchoMode_PasswordExceptLast: + m_drawer.AppendText(Nz::String(m_text.GetLength(), '*')); + break; + }*/ + + UpdateTextSprite(); + + SetCursorPosition(m_cursorPositionBegin); //< Refresh cursor position (prevent it from being outside of the text) + } +} diff --git a/SDK/src/NDK/Widgets/TextAreaWidget.cpp b/SDK/src/NDK/Widgets/TextAreaWidget.cpp index 03a6c30b6..6860a5139 100644 --- a/SDK/src/NDK/Widgets/TextAreaWidget.cpp +++ b/SDK/src/NDK/Widgets/TextAreaWidget.cpp @@ -10,46 +10,14 @@ namespace Ndk { - namespace - { - constexpr float paddingWidth = 5.f; - constexpr float paddingHeight = 3.f; - } - TextAreaWidget::TextAreaWidget(BaseWidget* parent) : - BaseWidget(parent), - m_characterFilter(), - m_echoMode(EchoMode_Normal), - m_cursorPositionBegin(0U, 0U), - m_cursorPositionEnd(0U, 0U), - m_isLineWrapEnabled(false), - m_isMouseButtonDown(false), - m_multiLineEnabled(false), - m_readOnly(false), - m_tabEnabled(false) + AbstractTextAreaWidget(parent) { - m_textSprite = Nz::TextSprite::New(); - - m_textEntity = CreateEntity(); - m_textEntity->AddComponent().Attach(m_textSprite); - - auto& textNode = m_textEntity->AddComponent(); - textNode.SetParent(this); - textNode.SetPosition(paddingWidth, paddingHeight); - - m_cursorEntity = CreateEntity(); - m_cursorEntity->AddComponent(); - m_cursorEntity->AddComponent().SetParent(m_textEntity); - m_cursorEntity->GetComponent(); - m_cursorEntity->Enable(false); - - SetCursor(Nz::SystemCursor_Text); SetCharacterSize(GetCharacterSize()); //< Actualize minimum / preferred size - EnableBackground(true); Layout(); } - + void TextAreaWidget::AppendText(const Nz::String& text) { m_text += text; @@ -86,21 +54,14 @@ namespace Ndk OnTextChanged(this, m_text); } - void TextAreaWidget::EnableLineWrap(bool enable) + void TextAreaWidget::Clear() { - if (m_isLineWrapEnabled != enable) - { - m_isLineWrapEnabled = enable; + AbstractTextAreaWidget::Clear(); - if (enable) - m_drawer.SetMaxLineWidth(GetWidth()); - else - m_drawer.SetMaxLineWidth(std::numeric_limits::infinity()); - - UpdateTextSprite(); - } + m_text.Clear(); + OnTextChanged(this, m_text); } - + void TextAreaWidget::Erase(std::size_t firstGlyph, std::size_t lastGlyph) { if (firstGlyph > lastGlyph) @@ -130,50 +91,6 @@ namespace Ndk SetText(newText); } - void TextAreaWidget::EraseSelection() - { - if (!HasSelection()) - return; - - Erase(GetGlyphIndex(m_cursorPositionBegin), GetGlyphIndex(m_cursorPositionEnd)); - } - - Nz::Vector2ui TextAreaWidget::GetHoveredGlyph(float x, float y) const - { - auto& textNode = m_textEntity->GetComponent(); - Nz::Vector2f textPosition = Nz::Vector2f(textNode.GetPosition(Nz::CoordSys_Local)); - x -= textPosition.x; - y -= textPosition.y; - - std::size_t glyphCount = m_drawer.GetGlyphCount(); - if (glyphCount > 0) - { - std::size_t lineCount = m_drawer.GetLineCount(); - std::size_t line = 0U; - for (; line < lineCount - 1; ++line) - { - Nz::Rectf lineBounds = m_drawer.GetLine(line).bounds; - if (lineBounds.GetMaximum().y > y) - break; - } - - std::size_t upperLimit = (line != lineCount - 1) ? m_drawer.GetLine(line + 1).glyphIndex : glyphCount + 1; - - 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; - if (x < bounds.x + bounds.width * 0.75f) - break; - } - - return Nz::Vector2ui(Nz::Vector2(i - firstLineGlyph, line)); - } - - return Nz::Vector2ui::Zero(); - } - void TextAreaWidget::SetCharacterSize(unsigned int characterSize) { m_drawer.SetCharacterSize(characterSize); @@ -198,6 +115,7 @@ namespace Ndk { if (glyphPosition >= m_drawer.GetGlyphCount()) { + // It's faster to append than to insert in the middle AppendText(text); SetCursorPosition(m_drawer.GetGlyphCount()); } @@ -210,452 +128,109 @@ namespace Ndk } } - void TextAreaWidget::Layout() + Nz::AbstractTextDrawer& TextAreaWidget::GetTextDrawer() { - BaseWidget::Layout(); + return m_drawer; + } - if (m_isLineWrapEnabled) + const Nz::AbstractTextDrawer& TextAreaWidget::GetTextDrawer() const + { + return m_drawer; + } + + void TextAreaWidget::HandleIndentation(bool add) + { + if (add) + Write(Nz::String('\t')); + else { - m_drawer.SetMaxLineWidth(GetWidth()); - UpdateTextSprite(); - } + std::size_t currentGlyph = GetGlyphIndex(m_cursorPositionBegin); - RefreshCursor(); - } - - bool TextAreaWidget::IsFocusable() const - { - return !m_readOnly; - } - - void TextAreaWidget::OnFocusLost() - { - m_cursorEntity->Disable(); - } - - void TextAreaWidget::OnFocusReceived() - { - if (!m_readOnly) - m_cursorEntity->Enable(true); - } - - bool TextAreaWidget::OnKeyPressed(const Nz::WindowEvent::KeyEvent& key) - { - switch (key.code) - { - case Nz::Keyboard::Backspace: + if (currentGlyph > 0 && m_text[m_text.GetCharacterPosition(currentGlyph - 1U)] == '\t') // Check if previous glyph is a tab { - bool ignoreDefaultAction = false; - OnTextAreaKeyBackspace(this, &ignoreDefaultAction); + Erase(currentGlyph - 1U); - std::size_t cursorGlyphBegin = GetGlyphIndex(m_cursorPositionBegin); - std::size_t cursorGlyphEnd = GetGlyphIndex(m_cursorPositionEnd); - - if (ignoreDefaultAction || cursorGlyphEnd == 0) - return true; - - // 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 (cursorGlyphBegin > 1) - newText.Append(m_text.SubString(0, m_text.GetCharacterPosition(cursorGlyphBegin - 1) - 1)); - - if (cursorGlyphEnd < m_text.GetLength()) - newText.Append(m_text.SubString(m_text.GetCharacterPosition(cursorGlyphEnd))); - - // Move cursor before setting text (to prevent SetText to move our cursor) + if (m_cursorPositionBegin.x < static_cast(m_drawer.GetLineGlyphCount(m_cursorPositionBegin.y))) MoveCursor(-1); - - SetText(newText); - } - - return true; } - - case Nz::Keyboard::Delete: - { - if (HasSelection()) - EraseSelection(); - else - Erase(GetGlyphIndex(m_cursorPositionBegin)); - - return true; - } - - case Nz::Keyboard::Down: - { - bool ignoreDefaultAction = false; - OnTextAreaKeyDown(this, &ignoreDefaultAction); - - 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; - - std::size_t lineCount = m_drawer.GetLineCount(); - if (key.control && lineCount > 0) - SetCursorPosition({ static_cast(m_drawer.GetLineGlyphCount(lineCount - 1)), static_cast(lineCount - 1) }); - else - SetCursorPosition({ static_cast(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, key.control ? 0U : m_cursorPositionEnd.y }); - return true; - } - - case Nz::Keyboard::Left: - { - bool ignoreDefaultAction = false; - OnTextAreaKeyLeft(this, &ignoreDefaultAction); - - if (ignoreDefaultAction) - return true; - - if (HasSelection()) - SetCursorPosition(m_cursorPositionBegin); - else if (key.control) - { - std::size_t index = GetGlyphIndex(m_cursorPositionBegin); - - if (index == 0) - return true; - - std::size_t spaceIndex = m_text.FindLast(' ', index - 2); - std::size_t endlIndex = m_text.FindLast('\n', index - 1); - - if ((spaceIndex > endlIndex || endlIndex == Nz::String::npos) && spaceIndex != Nz::String::npos) - SetCursorPosition(spaceIndex + 1); - else if (endlIndex != Nz::String::npos) - { - if (index == endlIndex + 1) - SetCursorPosition(endlIndex); - else - SetCursorPosition(endlIndex + 1); - } - else - SetCursorPosition({ 0U, m_cursorPositionBegin.y }); - } - else - MoveCursor(-1); - - return true; - } - - case Nz::Keyboard::Return: - { - bool ignoreDefaultAction = false; - OnTextAreaKeyReturn(this, &ignoreDefaultAction); - - if (ignoreDefaultAction) - return true; - - if (!m_multiLineEnabled) - break; - - if (HasSelection()) - EraseSelection(); - - Write(Nz::String('\n')); - return true;; - } - - case Nz::Keyboard::Right: - { - bool ignoreDefaultAction = false; - OnTextAreaKeyRight(this, &ignoreDefaultAction); - - if (ignoreDefaultAction) - return true; - - if (HasSelection()) - SetCursorPosition(m_cursorPositionEnd); - else if (key.control) - { - 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 != Nz::String::npos) - { - if (m_text.GetSize() > spaceIndex) - SetCursorPosition(spaceIndex + 1); - else - SetCursorPosition({ static_cast(m_drawer.GetLineGlyphCount(m_cursorPositionEnd.y)), m_cursorPositionEnd.y }); - } - else if (endlIndex != Nz::String::npos) - { - if (index == endlIndex) - SetCursorPosition(endlIndex + 1); - else - SetCursorPosition(endlIndex); - } - else - SetCursorPosition({ static_cast(m_drawer.GetLineGlyphCount(m_cursorPositionEnd.y)), m_cursorPositionEnd.y }); - } - else - MoveCursor(1); - - return true; - } - - case Nz::Keyboard::Up: - { - bool ignoreDefaultAction = false; - OnTextAreaKeyUp(this, &ignoreDefaultAction); - - if (ignoreDefaultAction) - return true; - - if (HasSelection()) - SetCursorPosition(m_cursorPositionBegin); - - MoveCursor({0, -1}); - return true; - } - - case Nz::Keyboard::Tab: - { - if (!m_tabEnabled) - return false; - - if (HasSelection()) - { - for(unsigned line = m_cursorPositionBegin.y; line <= m_cursorPositionEnd.y; ++line) - { - const Nz::Vector2ui cursorPositionBegin = m_cursorPositionBegin; - const Nz::Vector2ui cursorPositionEnd = m_cursorPositionEnd; - - if (key.shift) - { - if (m_drawer.GetLineGlyphCount(line) == 0) - continue; - - std::size_t firstGlyph = GetGlyphIndex({ 0U, line }); - - if (m_text[m_text.GetCharacterPosition(firstGlyph)] == '\t') - { - Erase(firstGlyph); - SetSelection(cursorPositionBegin - (cursorPositionBegin.y == line && cursorPositionBegin.x != 0U ? Nz::Vector2ui { 1U, 0U } : Nz::Vector2ui {}), - cursorPositionEnd - (cursorPositionEnd.y == line && cursorPositionEnd.x != 0U ? Nz::Vector2ui { 1U, 0U } : Nz::Vector2ui {})); - } - } - else - { - Write(Nz::String('\t'), { 0U, line }); - SetSelection(cursorPositionBegin + (cursorPositionBegin.y == line && cursorPositionBegin.x != 0U ? Nz::Vector2ui { 1U, 0U } : Nz::Vector2ui {}), - cursorPositionEnd + (cursorPositionEnd.y == line ? Nz::Vector2ui { 1U, 0U } : Nz::Vector2ui {})); - } - } - } - else if (key.shift) - { - std::size_t currentGlyph = GetGlyphIndex(m_cursorPositionBegin); - - if (currentGlyph > 0 && m_text[m_text.GetCharacterPosition(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); - } - } - else - Write(Nz::String('\t')); - - return true; - } - - default: - break; } - - return false; } - void TextAreaWidget::OnKeyReleased(const Nz::WindowEvent::KeyEvent& /*key*/) + void TextAreaWidget::HandleSelectionIndentation(bool add) { - } - - void TextAreaWidget::OnMouseButtonPress(int x, int y, Nz::Mouse::Button button) - { - if (button == Nz::Mouse::Left) + for (unsigned line = m_cursorPositionBegin.y; line <= m_cursorPositionEnd.y; ++line) { - SetFocus(); + const Nz::Vector2ui cursorPositionBegin = m_cursorPositionBegin; + const Nz::Vector2ui cursorPositionEnd = m_cursorPositionEnd; - Nz::Vector2ui hoveredGlyph = GetHoveredGlyph(float(x), float(y)); - - // Shift extends selection - if (Nz::Keyboard::IsKeyPressed(Nz::Keyboard::LShift) || Nz::Keyboard::IsKeyPressed(Nz::Keyboard::RShift)) - SetSelection(hoveredGlyph, m_selectionCursor); + if (add) + { + Write(Nz::String('\t'), { 0U, line }); + SetSelection(cursorPositionBegin + (cursorPositionBegin.y == line && cursorPositionBegin.x != 0U ? Nz::Vector2ui{ 1U, 0U } : Nz::Vector2ui{}), + cursorPositionEnd + (cursorPositionEnd.y == line ? Nz::Vector2ui{ 1U, 0U } : Nz::Vector2ui{})); + } else { - SetCursorPosition(hoveredGlyph); - m_selectionCursor = m_cursorPositionBegin; - } + if (m_drawer.GetLineGlyphCount(line) == 0) + continue; - m_isMouseButtonDown = true; + std::size_t firstGlyph = GetGlyphIndex({ 0U, line }); + + if (m_text[m_text.GetCharacterPosition(firstGlyph)] == '\t') + { + Erase(firstGlyph); + SetSelection(cursorPositionBegin - (cursorPositionBegin.y == line && cursorPositionBegin.x != 0U ? Nz::Vector2ui{ 1U, 0U } : Nz::Vector2ui{}), + cursorPositionEnd - (cursorPositionEnd.y == line && cursorPositionEnd.x != 0U ? Nz::Vector2ui{ 1U, 0U } : Nz::Vector2ui{})); + } + } } } - void TextAreaWidget::OnMouseButtonRelease(int, int, Nz::Mouse::Button button) + void TextAreaWidget::HandleWordCursorMove(bool left) { - 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) - SetSelection(m_selectionCursor, GetHoveredGlyph(float(x), float(y))); - } - - void TextAreaWidget::OnTextEntered(char32_t character, bool /*repeated*/) - { - if (m_readOnly) - return; - - if (Nz::Unicode::GetCategory(character) == Nz::Unicode::Category_Other_Control || (m_characterFilter && !m_characterFilter(character))) - return; - - if (HasSelection()) - EraseSelection(); - - Write(Nz::String::Unicode(character)); - } - - void TextAreaWidget::RefreshCursor() - { - if (m_readOnly) - return; - - auto GetGlyph = [&](const Nz::Vector2ui& glyphPosition, std::size_t* glyphIndex) -> const Nz::AbstractTextDrawer::Glyph* + if (left) { - const auto& lineInfo = m_drawer.GetLine(glyphPosition.y); + std::size_t index = GetGlyphIndex(m_cursorPositionBegin); + if (index == 0) + return; - std::size_t cursorGlyph = GetGlyphIndex({ glyphPosition.x, glyphPosition.y }); - if (glyphIndex) - *glyphIndex = cursorGlyph; + std::size_t spaceIndex = m_text.FindLast(' ', index - 2); + std::size_t endlIndex = m_text.FindLast('\n', index - 1); - std::size_t glyphCount = m_drawer.GetGlyphCount(); - if (glyphCount > 0 && lineInfo.glyphIndex < cursorGlyph) + if ((spaceIndex > endlIndex || endlIndex == Nz::String::npos) && spaceIndex != Nz::String::npos) + SetCursorPosition(spaceIndex + 1); + else if (endlIndex != Nz::String::npos) { - const auto& glyph = m_drawer.GetGlyph(std::min(cursorGlyph, glyphCount - 1)); - return &glyph; + if (index == endlIndex + 1) + SetCursorPosition(endlIndex); + else + SetCursorPosition(endlIndex + 1); } else - return nullptr; - }; - - // Move text so that cursor is always visible - const auto* lastGlyph = GetGlyph(m_cursorPositionEnd, nullptr); - float glyphPos = (lastGlyph) ? lastGlyph->bounds.x : 0.f; - float glyphWidth = (lastGlyph) ? lastGlyph->bounds.width : 0.f; - - auto& node = m_textEntity->GetComponent(); - float textPosition = node.GetPosition(Nz::CoordSys_Local).x - paddingWidth; - float cursorPosition = glyphPos + textPosition; - float width = GetWidth(); - - if (width <= m_drawer.GetBounds().width) - { - if (cursorPosition + glyphWidth > width) - node.Move(width - cursorPosition - glyphWidth, 0.f); - else if (cursorPosition - glyphWidth < 0.f) - node.Move(-cursorPosition + glyphWidth, 0.f); + SetCursorPosition({ 0U, m_cursorPositionBegin.y }); } else - node.Move(-textPosition, 0.f); // Reset text position if we have enough room to show everything - - // Show cursor/selection - std::size_t selectionLineCount = m_cursorPositionEnd.y - m_cursorPositionBegin.y + 1; - std::size_t oldSpriteCount = m_cursorSprites.size(); - if (m_cursorSprites.size() != selectionLineCount) { - m_cursorSprites.resize(m_cursorPositionEnd.y - m_cursorPositionBegin.y + 1); - for (std::size_t i = oldSpriteCount; i < m_cursorSprites.size(); ++i) + 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 != Nz::String::npos) { - m_cursorSprites[i] = Nz::Sprite::New(); - m_cursorSprites[i]->SetMaterial(Nz::Material::New("Translucent2D")); + if (m_text.GetSize() > spaceIndex) + SetCursorPosition(spaceIndex + 1); + else + SetCursorPosition({ static_cast(m_drawer.GetLineGlyphCount(m_cursorPositionEnd.y)), m_cursorPositionEnd.y }); } - } - - float lineHeight = float(m_drawer.GetFont()->GetSizeInfo(m_drawer.GetCharacterSize()).lineHeight); - - 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) + else if (endlIndex != Nz::String::npos) { - auto GetGlyphPos = [&](const Nz::Vector2ui& glyphPosition) - { - std::size_t glyphIndex; - const auto* glyph = GetGlyph(glyphPosition, &glyphIndex); - if (glyph) - { - float position = glyph->bounds.x; - if (glyphIndex >= m_drawer.GetGlyphCount()) - position += glyph->bounds.width; - - return position; - } - else - return 0.f; - }; - - float beginX = (i == m_cursorPositionBegin.y) ? GetGlyphPos({ m_cursorPositionBegin.x, i }) : 0.f; - float endX = (i == m_cursorPositionEnd.y) ? GetGlyphPos({ m_cursorPositionEnd.x, i }) : 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 })); + if (index == endlIndex) + SetCursorPosition(endlIndex + 1); + else + SetCursorPosition(endlIndex); } 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 })); - } + SetCursorPosition({ static_cast(m_drawer.GetLineGlyphCount(m_cursorPositionEnd.y)), m_cursorPositionEnd.y }); } } @@ -677,10 +252,4 @@ namespace Ndk SetCursorPosition(m_cursorPositionBegin); //< Refresh cursor position (prevent it from being outside of the text) } - - void TextAreaWidget::UpdateTextSprite() - { - m_textSprite->Update(m_drawer); - SetPreferredSize(Nz::Vector2f(m_textSprite->GetBoundingVolume().obb.localBox.GetLengths())); - } } diff --git a/include/Nazara/Utility.hpp b/include/Nazara/Utility.hpp index 2ccf84080..97deaca58 100644 --- a/include/Nazara/Utility.hpp +++ b/include/Nazara/Utility.hpp @@ -54,6 +54,7 @@ #include #include #include +#include #include #include #include diff --git a/include/Nazara/Utility/AbstractTextDrawer.hpp b/include/Nazara/Utility/AbstractTextDrawer.hpp index 0b45d422d..37204897a 100644 --- a/include/Nazara/Utility/AbstractTextDrawer.hpp +++ b/include/Nazara/Utility/AbstractTextDrawer.hpp @@ -27,6 +27,8 @@ namespace Nz AbstractTextDrawer() = default; virtual ~AbstractTextDrawer(); + virtual void Clear() = 0; + virtual const Recti& GetBounds() const = 0; virtual Font* GetFont(std::size_t index) const = 0; virtual std::size_t GetFontCount() const = 0; @@ -35,6 +37,9 @@ namespace Nz virtual const Line& GetLine(std::size_t index) const = 0; virtual std::size_t GetLineCount() const = 0; inline std::size_t GetLineGlyphCount(std::size_t index) const; + virtual float GetMaxLineWidth() const = 0; + + virtual void SetMaxLineWidth(float lineWidth) = 0; struct Glyph { diff --git a/include/Nazara/Utility/RichTextDrawer.hpp b/include/Nazara/Utility/RichTextDrawer.hpp index 3a681e20d..c2d288109 100644 --- a/include/Nazara/Utility/RichTextDrawer.hpp +++ b/include/Nazara/Utility/RichTextDrawer.hpp @@ -26,29 +26,33 @@ namespace Nz RichTextDrawer(RichTextDrawer&& drawer); ~RichTextDrawer(); - BlockRef AppendText(const String& str); + BlockRef AppendText(const String& str, bool forceNewBlock = false); - inline void Clear(); + void Clear() override; + + inline std::size_t FindBlock(std::size_t glyphIndex) const; inline unsigned int GetBlockCharacterSize(std::size_t index) const; inline const Color& GetBlockColor(std::size_t index) const; inline std::size_t GetBlockCount() const; + inline std::size_t GetBlockFirstGlyphIndex(std::size_t index) const; inline const FontRef& GetBlockFont(std::size_t index) const; inline TextStyleFlags GetBlockStyle(std::size_t index) const; inline const String& GetBlockText(std::size_t index) const; + inline BlockRef GetBlock(std::size_t index); + const Recti& GetBounds() const override; inline unsigned int GetDefaultCharacterSize() const; inline const Color& GetDefaultColor() const; inline const FontRef& GetDefaultFont() const; inline TextStyleFlags GetDefaultStyle() const; - - const Recti& GetBounds() const override; Font* GetFont(std::size_t index) const override; std::size_t GetFontCount() const override; const Glyph& GetGlyph(std::size_t index) const override; std::size_t GetGlyphCount() const override; const Line& GetLine(std::size_t index) const override; std::size_t GetLineCount() const override; + float GetMaxLineWidth() const override; void MergeBlocks(); @@ -65,9 +69,13 @@ namespace Nz inline void SetDefaultFont(const FontRef& font); inline void SetDefaultStyle(TextStyleFlags style); + void SetMaxLineWidth(float lineWidth) override; + RichTextDrawer& operator=(const RichTextDrawer& drawer); RichTextDrawer& operator=(RichTextDrawer&& drawer); + static constexpr std::size_t InvalidBlockIndex = std::numeric_limits::max(); + //static RichTextDrawer Draw(const String& str, unsigned int characterSize, TextStyleFlags style = TextStyle_Regular, const Color& color = Color::White); //static RichTextDrawer Draw(Font* font, const String& str, unsigned int characterSize, TextStyleFlags style = TextStyle_Regular, const Color& color = Color::White); @@ -94,6 +102,7 @@ namespace Nz struct Block { std::size_t fontIndex; + std::size_t glyphIndex; Color color; String text; TextStyleFlags style; @@ -123,6 +132,7 @@ namespace Nz mutable Recti m_bounds; mutable Vector2ui m_drawPos; mutable bool m_glyphUpdated; + float m_maxLineWidth; unsigned int m_defaultCharacterSize; }; @@ -137,6 +147,7 @@ namespace Nz inline unsigned int GetCharacterSize() const; inline Color GetColor() const; + inline std::size_t GetFirstGlyphIndex() const; inline const FontRef& GetFont() const; inline TextStyleFlags GetStyle() const; inline const String& GetText() const; diff --git a/include/Nazara/Utility/RichTextDrawer.inl b/include/Nazara/Utility/RichTextDrawer.inl index c2591a20b..f258d0596 100644 --- a/include/Nazara/Utility/RichTextDrawer.inl +++ b/include/Nazara/Utility/RichTextDrawer.inl @@ -7,13 +7,38 @@ namespace Nz { - inline void RichTextDrawer::Clear() + inline std::size_t RichTextDrawer::FindBlock(std::size_t glyphIndex) const { - m_fontIndexes.clear(); - m_blocks.clear(); - m_fonts.clear(); - m_glyphs.clear(); - ClearGlyphs(); + // Binary search + std::size_t count = m_blocks.size(); + std::size_t step; + + std::size_t i = InvalidBlockIndex; + std::size_t first = 0; + std::size_t last = count; + while (count > 0) + { + i = first; + step = count / 2; + + i += step; + + if (m_blocks[i].glyphIndex < glyphIndex) + { + first = i + 1; + count -= step + 1; + } + else + count = step; + } + + return i; + } + + inline auto RichTextDrawer::GetBlock(std::size_t index) -> BlockRef + { + NazaraAssert(index < m_blocks.size(), "Invalid block index"); + return BlockRef(*this, index); } inline unsigned int RichTextDrawer::GetBlockCharacterSize(std::size_t index) const @@ -33,6 +58,12 @@ namespace Nz return m_blocks.size(); } + inline std::size_t RichTextDrawer::GetBlockFirstGlyphIndex(std::size_t index) const + { + NazaraAssert(index < m_blocks.size(), "Invalid block index"); + return m_blocks[index].glyphIndex; + } + inline const FontRef& RichTextDrawer::GetBlockFont(std::size_t index) const { NazaraAssert(index < m_blocks.size(), "Invalid block index"); @@ -204,8 +235,19 @@ namespace Nz inline void RichTextDrawer::SetBlockText(std::size_t index, const String& str) { NazaraAssert(index < m_blocks.size(), "Invalid block index"); + + std::size_t previousLength = m_blocks[index].text.GetLength(); + m_blocks[index].text = str; + std::size_t newLength = m_blocks[index].text.GetLength(); + if (newLength != previousLength) + { + std::size_t delta = newLength - previousLength; //< Underflow allowed + for (std::size_t i = index + 1; i < m_blocks.size(); ++i) + m_blocks[i].glyphIndex += delta; + } + InvalidateGlyphs(); } @@ -291,6 +333,17 @@ namespace Nz return m_drawer.GetBlockStyle(m_blockIndex); } + /*! + * Returns the first glyph index at which starts the referenced block + * \return The first glyph index concerned by this block + * + * \see GetText + */ + inline std::size_t RichTextDrawer::BlockRef::GetFirstGlyphIndex() const + { + return m_drawer.GetBlockFirstGlyphIndex(m_blockIndex); + } + /*! * Returns the text of the referenced block * \return The referenced block text diff --git a/include/Nazara/Utility/SimpleTextDrawer.hpp b/include/Nazara/Utility/SimpleTextDrawer.hpp index 525f49cc5..5e33ed79f 100644 --- a/include/Nazara/Utility/SimpleTextDrawer.hpp +++ b/include/Nazara/Utility/SimpleTextDrawer.hpp @@ -26,7 +26,7 @@ namespace Nz void AppendText(const String& str); - void Clear(); + void Clear() override; const Recti& GetBounds() const override; unsigned int GetCharacterSize() const; diff --git a/src/Nazara/Utility/RichTextDrawer.cpp b/src/Nazara/Utility/RichTextDrawer.cpp index 5480a5c16..481fd35d1 100644 --- a/src/Nazara/Utility/RichTextDrawer.cpp +++ b/src/Nazara/Utility/RichTextDrawer.cpp @@ -13,7 +13,7 @@ namespace Nz m_defaultColor(Color::White), //m_outlineColor(Color::Black), m_defaultStyle(TextStyle_Regular), - m_glyphUpdated(true), + m_glyphUpdated(false), //m_maxLineWidth(std::numeric_limits::infinity()), //m_outlineThickness(0.f), m_defaultCharacterSize(24) @@ -51,7 +51,7 @@ namespace Nz RichTextDrawer::~RichTextDrawer() = default; - auto RichTextDrawer::AppendText(const String& str) -> BlockRef + auto RichTextDrawer::AppendText(const String& str, bool forceNewBlock) -> BlockRef { NazaraAssert(!str.IsEmpty(), "String cannot be empty"); @@ -66,13 +66,23 @@ namespace Nz }; // Check if last block has the same property as default, else create a new block - if (m_blocks.empty() || !HasDefaultProperties(m_blocks.back())) + if (forceNewBlock || m_blocks.empty() || !HasDefaultProperties(m_blocks.back())) { + std::size_t glyphIndex; + if (!m_blocks.empty()) + { + Block& lastBlock = m_blocks.back(); + glyphIndex = lastBlock.glyphIndex + lastBlock.text.GetLength(); + } + else + glyphIndex = 0; + m_blocks.emplace_back(); Block& newBlock = m_blocks.back(); newBlock.characterSize = m_defaultCharacterSize; newBlock.color = m_defaultColor; newBlock.fontIndex = defaultFontIndex; + newBlock.glyphIndex = glyphIndex; newBlock.style = m_defaultStyle; newBlock.text = str; @@ -87,6 +97,15 @@ namespace Nz return BlockRef(*this, m_blocks.size() - 1); } + void RichTextDrawer::Clear() + { + m_fontIndexes.clear(); + m_blocks.clear(); + m_fonts.clear(); + m_glyphs.clear(); + ClearGlyphs(); + } + const Recti& RichTextDrawer::GetBounds() const { if (!m_glyphUpdated) @@ -140,6 +159,11 @@ namespace Nz return m_lines.size(); } + float RichTextDrawer::GetMaxLineWidth() const + { + return m_maxLineWidth; + } + void RichTextDrawer::MergeBlocks() { if (m_blocks.size() < 2) @@ -180,6 +204,13 @@ namespace Nz m_blocks.erase(m_blocks.begin() + index); } + void RichTextDrawer::SetMaxLineWidth(float lineWidth) + { + m_maxLineWidth = lineWidth; + + //TODO: Implement max line width + } + RichTextDrawer& RichTextDrawer::operator=(const RichTextDrawer& drawer) { DisconnectFontSlots(); @@ -472,5 +503,7 @@ namespace Nz GenerateGlyphs(fontData.font, block.color, block.style, block.characterSize, block.color, 0.f, block.text); } } + else + m_lines.emplace_back(Line{ Rectf::Zero(), 0 }); //< Ensure there's always a line } }