From caf1a0f1e8b25909ae82c3b0dc528847cefabb40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Leclercq?= Date: Wed, 24 Nov 2021 22:25:39 +0100 Subject: [PATCH] Widgets: Add [Rich]TextAreaWidget --- include/Nazara/Core/StringExt.hpp | 3 + include/Nazara/Widgets.hpp | 4 + .../Nazara/Widgets/AbstractTextAreaWidget.hpp | 138 +++++ .../Nazara/Widgets/AbstractTextAreaWidget.inl | 255 +++++++++ include/Nazara/Widgets/Enums.hpp | 37 ++ include/Nazara/Widgets/RichTextAreaWidget.hpp | 68 +++ include/Nazara/Widgets/RichTextAreaWidget.inl | 91 ++++ include/Nazara/Widgets/TextAreaWidget.hpp | 77 +++ include/Nazara/Widgets/TextAreaWidget.inl | 127 +++++ src/Nazara/Core/StringExt.cpp | 33 ++ src/Nazara/Widgets/AbstractTextAreaWidget.cpp | 510 ++++++++++++++++++ src/Nazara/Widgets/RichTextAreaWidget.cpp | 198 +++++++ src/Nazara/Widgets/TextAreaWidget.cpp | 255 +++++++++ 13 files changed, 1796 insertions(+) create mode 100644 include/Nazara/Widgets/AbstractTextAreaWidget.hpp create mode 100644 include/Nazara/Widgets/AbstractTextAreaWidget.inl create mode 100644 include/Nazara/Widgets/Enums.hpp create mode 100644 include/Nazara/Widgets/RichTextAreaWidget.hpp create mode 100644 include/Nazara/Widgets/RichTextAreaWidget.inl create mode 100644 include/Nazara/Widgets/TextAreaWidget.hpp create mode 100644 include/Nazara/Widgets/TextAreaWidget.inl create mode 100644 src/Nazara/Widgets/AbstractTextAreaWidget.cpp create mode 100644 src/Nazara/Widgets/RichTextAreaWidget.cpp create mode 100644 src/Nazara/Widgets/TextAreaWidget.cpp diff --git a/include/Nazara/Core/StringExt.hpp b/include/Nazara/Core/StringExt.hpp index 9b37ee773..5bc1eddce 100644 --- a/include/Nazara/Core/StringExt.hpp +++ b/include/Nazara/Core/StringExt.hpp @@ -18,10 +18,13 @@ namespace Nz struct UnicodeAware {}; // std::string is assumed to contains UTF-8 + NAZARA_CORE_API std::size_t ComputeCharacterCount(const std::string_view& str); + NAZARA_CORE_API std::string FromUtf16String(const std::u16string_view& u16str); NAZARA_CORE_API std::string FromUtf32String(const std::u32string_view& u32str); NAZARA_CORE_API std::string FromWideString(const std::wstring_view& str); + NAZARA_CORE_API std::size_t GetCharacterPosition(const std::string_view& str, std::size_t characterIndex); NAZARA_CORE_API std::string_view GetWord(const std::string_view& str, std::size_t wordIndex); NAZARA_CORE_API std::string_view GetWord(const std::string_view& str, std::size_t wordIndex, UnicodeAware); diff --git a/include/Nazara/Widgets.hpp b/include/Nazara/Widgets.hpp index 22ddb0396..a9862c07a 100644 --- a/include/Nazara/Widgets.hpp +++ b/include/Nazara/Widgets.hpp @@ -29,12 +29,16 @@ #ifndef NAZARA_GLOBAL_WIDGETS_HPP #define NAZARA_GLOBAL_WIDGETS_HPP +#include #include #include #include #include +#include #include #include +#include +#include #include #endif // NAZARA_GLOBAL_WIDGETS_HPP diff --git a/include/Nazara/Widgets/AbstractTextAreaWidget.hpp b/include/Nazara/Widgets/AbstractTextAreaWidget.hpp new file mode 100644 index 000000000..9c5cc2ca0 --- /dev/null +++ b/include/Nazara/Widgets/AbstractTextAreaWidget.hpp @@ -0,0 +1,138 @@ +// Copyright (C) 2021 Jérôme "Lynix" Leclercq (lynix680@gmail.com) +// This file is part of the "Nazara Engine - Widgets module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#pragma once + +#ifndef NAZARA_WIDGETS_ABSTRACTTEXTAREAWIDGET_HPP +#define NAZARA_WIDGETS_ABSTRACTTEXTAREAWIDGET_HPP + +#include +#include +#include +#include +#include +#include + +namespace Nz +{ + class NAZARA_WIDGETS_API AbstractTextAreaWidget : public BaseWidget + { + public: + using CharacterFilter = std::function; + + AbstractTextAreaWidget(BaseWidget* parent); + AbstractTextAreaWidget(const AbstractTextAreaWidget&) = delete; + AbstractTextAreaWidget(AbstractTextAreaWidget&&) = default; + ~AbstractTextAreaWidget() = default; + + virtual void Clear(); + + 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 Vector2ui& GetCursorPosition() const; + inline Vector2ui GetCursorPosition(std::size_t glyphIndex) const; + inline EchoMode GetEchoMode() const; + inline std::size_t GetGlyphIndex() const; + inline std::size_t GetGlyphIndex(const Vector2ui& cursorPosition) const; + inline const std::string& GetText() const; + + 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 Vector2i& offset); + + inline Vector2ui NormalizeCursorPosition(Vector2ui cursorPosition) const; + + inline void SetCharacterFilter(CharacterFilter filter); + inline void SetCursorPosition(std::size_t glyphIndex); + inline void SetCursorPosition(Vector2ui cursorPosition); + inline void SetEchoMode(EchoMode echoMode); + inline void SetReadOnly(bool readOnly = true); + inline void SetSelection(Vector2ui fromPosition, Vector2ui toPosition); + + inline void Write(const std::string& text); + inline void Write(const std::string& text, const Vector2ui& glyphPosition); + virtual void Write(const std::string& text, std::size_t glyphPosition) = 0; + + AbstractTextAreaWidget& operator=(const AbstractTextAreaWidget&) = delete; + AbstractTextAreaWidget& operator=(AbstractTextAreaWidget&&) = default; + + NazaraSignal(OnTextAreaCursorMove, const AbstractTextAreaWidget* /*textArea*/, 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*/, Vector2ui* /*start*/, Vector2ui* /*end*/); + + protected: + virtual AbstractTextDrawer& GetTextDrawer() = 0; + virtual const 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 WindowEvent::KeyEvent& key) override; + void OnKeyReleased(const WindowEvent::KeyEvent& key) override; + void OnMouseButtonPress(int /*x*/, int /*y*/, Mouse::Button button) override; + void OnMouseButtonRelease(int /*x*/, int /*y*/, 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(Vector2ui cursorPosition); + + void RefreshCursor(); + virtual void UpdateDisplayText() = 0; + void UpdateTextSprite(); + + struct Cursor + { + std::shared_ptr sprite; + entt::entity entity; + }; + + std::shared_ptr m_textSprite; + std::vector m_cursors; + CharacterFilter m_characterFilter; + EchoMode m_echoMode; + entt::entity m_textEntity; + Vector2ui m_cursorPositionBegin; + Vector2ui m_cursorPositionEnd; + Vector2ui m_selectionCursor; + 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 // NAZARA_WIDGETS_ABSTRACTTEXTAREAWIDGET_HPP diff --git a/include/Nazara/Widgets/AbstractTextAreaWidget.inl b/include/Nazara/Widgets/AbstractTextAreaWidget.inl new file mode 100644 index 000000000..e333aad9c --- /dev/null +++ b/include/Nazara/Widgets/AbstractTextAreaWidget.inl @@ -0,0 +1,255 @@ +// Copyright (C) 2021 Jérôme "Lynix" Leclercq (lynix680@gmail.com) +// This file is part of the "Nazara Engine - Widgets module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#include +#include + +namespace Nz +{ + 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 Vector2ui& AbstractTextAreaWidget::GetCursorPosition() const + { + return m_cursorPositionBegin; + } + + Vector2ui AbstractTextAreaWidget::GetCursorPosition(std::size_t glyphIndex) const + { + const 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); + + 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 Vector2ui& cursorPosition) const + { + const 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 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; + } + }; + + 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 Vector2ui AbstractTextAreaWidget::NormalizeCursorPosition(Vector2ui cursorPosition) const + { + const 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) + { + Vector2ui position = GetCursorPosition(glyphIndex); + Vector2ui newPosition = position; + + OnTextAreaCursorMove(this, &newPosition); + + if (position == newPosition) + SetCursorPositionInternal(position); + else + SetCursorPositionInternal(GetGlyphIndex(newPosition)); + } + + inline void AbstractTextAreaWidget::SetCursorPosition(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(Vector2ui fromPosition, 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 std::string& text) + { + Write(text, GetGlyphIndex(m_cursorPositionBegin)); + } + + inline void AbstractTextAreaWidget::Write(const std::string& text, const Vector2ui& glyphPosition) + { + Write(text, GetGlyphIndex(glyphPosition)); + } + + void AbstractTextAreaWidget::SetCursorPositionInternal(std::size_t glyphIndex) + { + return SetCursorPositionInternal(GetCursorPosition(glyphIndex)); + } + + inline void AbstractTextAreaWidget::SetCursorPositionInternal(Vector2ui cursorPosition) + { + m_cursorPositionBegin = cursorPosition; + m_cursorPositionEnd = m_cursorPositionBegin; + + RefreshCursor(); + } +} + +#include diff --git a/include/Nazara/Widgets/Enums.hpp b/include/Nazara/Widgets/Enums.hpp new file mode 100644 index 000000000..2bf9640ff --- /dev/null +++ b/include/Nazara/Widgets/Enums.hpp @@ -0,0 +1,37 @@ +// Copyright (C) 2021 Samy Bensaid +// This file is part of the "Nazara Engine - Widgets module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#pragma once + +#ifndef NAZARA_WIDGETS_ENUMS_HPP +#define NAZARA_WIDGETS_ENUMS_HPP + +namespace Nz +{ + enum class BoxLayoutOrientation + { + Horizontal, + Vertical + }; + + enum class CheckboxState + { + Checked, + Tristate, + Unchecked, + + Max = Unchecked + }; + + enum class EchoMode + { + Hidden, + HiddenExceptLast, + Normal, + + Max = Normal + }; +} + +#endif // NAZARA_WIDGETS_ENUMS_HPP diff --git a/include/Nazara/Widgets/RichTextAreaWidget.hpp b/include/Nazara/Widgets/RichTextAreaWidget.hpp new file mode 100644 index 000000000..2952510e7 --- /dev/null +++ b/include/Nazara/Widgets/RichTextAreaWidget.hpp @@ -0,0 +1,68 @@ +// Copyright (C) 2021 Jérôme "Lynix" Leclercq (lynix680@gmail.com) +// This file is part of the "Nazara Engine - Widgets module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#pragma once + +#ifndef NAZARA_WIDGETS_RICHTEXTAREAWIDGET_HPP +#define NAZARA_WIDGETS_RICHTEXTAREAWIDGET_HPP + +#include +#include + +namespace Nz +{ + class NAZARA_WIDGETS_API RichTextAreaWidget : public AbstractTextAreaWidget + { + public: + RichTextAreaWidget(BaseWidget* parent); + RichTextAreaWidget(const RichTextAreaWidget&) = delete; + RichTextAreaWidget(RichTextAreaWidget&&) = default; + ~RichTextAreaWidget() = default; + + void AppendText(const std::string& text); + + void Clear() override; + + void Erase(std::size_t firstGlyph, std::size_t lastGlyph) override; + + inline unsigned int GetCharacterSize() const; + inline float GetCharacterSpacingOffset() const; + inline float GetLineSpacingOffset() const; + inline const Color& GetTextColor() const; + inline const std::shared_ptr& GetTextFont() const; + inline const Color& GetTextOutlineColor() const; + inline float GetTextOutlineThickness() const; + inline TextStyleFlags GetTextStyle() const; + + inline void SetCharacterSize(unsigned int characterSize); + inline void SetCharacterSpacingOffset(float offset); + inline void SetLineSpacingOffset(float offset); + inline void SetTextColor(const Color& color); + inline void SetTextFont(std::shared_ptr font); + inline void SetTextOutlineColor(const Color& color); + inline void SetTextOutlineThickness(float thickness); + inline void SetTextStyle(TextStyleFlags style); + + void Write(const std::string& text, std::size_t glyphPosition) override; + + RichTextAreaWidget& operator=(const RichTextAreaWidget&) = delete; + RichTextAreaWidget& operator=(RichTextAreaWidget&&) = default; + + private: + AbstractTextDrawer& GetTextDrawer() override; + const AbstractTextDrawer& GetTextDrawer() const override; + + void HandleIndentation(bool add) override; + void HandleSelectionIndentation(bool add) override; + void HandleWordCursorMove(bool left) override; + + void UpdateDisplayText() override; + + RichTextDrawer m_drawer; + }; +} + +#include + +#endif // NAZARA_WIDGETS_RICHTEXTAREAWIDGET_HPP diff --git a/include/Nazara/Widgets/RichTextAreaWidget.inl b/include/Nazara/Widgets/RichTextAreaWidget.inl new file mode 100644 index 000000000..9baac7837 --- /dev/null +++ b/include/Nazara/Widgets/RichTextAreaWidget.inl @@ -0,0 +1,91 @@ +// Copyright (C) 2021 Jérôme "Lynix" Leclercq (lynix680@gmail.com) +// This file is part of the "Nazara Engine - Widgets module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#include +#include + +namespace Nz +{ + inline unsigned int RichTextAreaWidget::GetCharacterSize() const + { + return m_drawer.GetDefaultCharacterSize(); + } + + inline float RichTextAreaWidget::GetCharacterSpacingOffset() const + { + return m_drawer.GetDefaultCharacterSpacingOffset(); + } + + inline float RichTextAreaWidget::GetLineSpacingOffset() const + { + return m_drawer.GetDefaultLineSpacingOffset(); + } + + inline const Color& RichTextAreaWidget::GetTextColor() const + { + return m_drawer.GetDefaultColor(); + } + + inline const std::shared_ptr& RichTextAreaWidget::GetTextFont() const + { + return m_drawer.GetDefaultFont(); + } + + inline const Color& RichTextAreaWidget::GetTextOutlineColor() const + { + return m_drawer.GetDefaultOutlineColor(); + } + + inline float RichTextAreaWidget::GetTextOutlineThickness() const + { + return m_drawer.GetDefaultOutlineThickness(); + } + + inline TextStyleFlags RichTextAreaWidget::GetTextStyle() const + { + return m_drawer.GetDefaultStyle(); + } + + inline void RichTextAreaWidget::SetCharacterSize(unsigned int characterSize) + { + m_drawer.SetDefaultCharacterSize(characterSize); + } + + inline void RichTextAreaWidget::SetCharacterSpacingOffset(float offset) + { + m_drawer.SetDefaultCharacterSpacingOffset(offset); + } + + inline void RichTextAreaWidget::SetLineSpacingOffset(float offset) + { + m_drawer.SetDefaultLineSpacingOffset(offset); + } + + inline void RichTextAreaWidget::SetTextColor(const Color& color) + { + m_drawer.SetDefaultColor(color); + } + + inline void RichTextAreaWidget::SetTextFont(std::shared_ptr font) + { + m_drawer.SetDefaultFont(std::move(font)); + } + + inline void RichTextAreaWidget::SetTextOutlineColor(const Color& color) + { + m_drawer.SetDefaultOutlineColor(color); + } + + inline void RichTextAreaWidget::SetTextOutlineThickness(float thickness) + { + m_drawer.SetDefaultOutlineThickness(thickness); + } + + inline void RichTextAreaWidget::SetTextStyle(TextStyleFlags style) + { + m_drawer.SetDefaultStyle(style); + } +} + +#include diff --git a/include/Nazara/Widgets/TextAreaWidget.hpp b/include/Nazara/Widgets/TextAreaWidget.hpp new file mode 100644 index 000000000..f9dbbd024 --- /dev/null +++ b/include/Nazara/Widgets/TextAreaWidget.hpp @@ -0,0 +1,77 @@ +// Copyright (C) 2021 Jérôme "Lynix" Leclercq (lynix680@gmail.com) +// This file is part of the "Nazara Engine - Widgets module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#pragma once + +#ifndef NAZARA_WIDGETS_TEXTAREAWIDGET_HPP +#define NAZARA_WIDGETS_TEXTAREAWIDGET_HPP + +#include +#include + +namespace Nz +{ + class NAZARA_WIDGETS_API TextAreaWidget : public AbstractTextAreaWidget + { + public: + TextAreaWidget(BaseWidget* parent); + TextAreaWidget(const TextAreaWidget&) = delete; + TextAreaWidget(TextAreaWidget&&) = default; + ~TextAreaWidget() = default; + + void AppendText(const std::string& text); + + void Clear() override; + + using AbstractTextAreaWidget::Erase; + void Erase(std::size_t firstGlyph, std::size_t lastGlyph) override; + + inline unsigned int GetCharacterSize() const; + inline const std::string& GetDisplayText() const; + inline float GetCharacterSpacingOffset() const; + inline float GetLineSpacingOffset() const; + inline const std::string& GetText() const; + inline const Color& GetTextColor() const; + inline const std::shared_ptr& GetTextFont() const; + inline const Color& GetTextOulineColor() const; + inline float GetTextOulineThickness() const; + inline TextStyleFlags GetTextStyle() const; + + inline void SetCharacterSize(unsigned int characterSize); + inline void SetCharacterSpacingOffset(float offset); + inline void SetLineSpacingOffset(float offset); + inline void SetText(const std::string& text); + inline void SetTextColor(const Color& text); + inline void SetTextFont(std::shared_ptr font); + inline void SetTextOutlineColor(const Color& color); + inline void SetTextOutlineThickness(float thickness); + inline void SetTextStyle(TextStyleFlags style); + + using AbstractTextAreaWidget::Write; + void Write(const std::string& text, std::size_t glyphPosition) override; + + TextAreaWidget& operator=(const TextAreaWidget&) = delete; + TextAreaWidget& operator=(TextAreaWidget&&) = default; + + NazaraSignal(OnTextChanged, const AbstractTextAreaWidget* /*textArea*/, const std::string& /*text*/); + + private: + AbstractTextDrawer& GetTextDrawer() override; + const AbstractTextDrawer& GetTextDrawer() const override; + + void HandleIndentation(bool add) override; + void HandleSelectionIndentation(bool add) override; + void HandleWordCursorMove(bool left) override; + + void UpdateDisplayText() override; + void UpdateMinimumSize(); + + SimpleTextDrawer m_drawer; + std::string m_text; + }; +} + +#include + +#endif // NAZARA_WIDGETS_TEXTAREAWIDGET_HPP diff --git a/include/Nazara/Widgets/TextAreaWidget.inl b/include/Nazara/Widgets/TextAreaWidget.inl new file mode 100644 index 000000000..99a815e40 --- /dev/null +++ b/include/Nazara/Widgets/TextAreaWidget.inl @@ -0,0 +1,127 @@ +// Copyright (C) 2021 Jérôme "Lynix" Leclercq (lynix680@gmail.com) +// This file is part of the "Nazara Engine - Widgets module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#include +#include + +namespace Nz +{ + inline unsigned int TextAreaWidget::GetCharacterSize() const + { + return m_drawer.GetCharacterSize(); + } + + inline const std::string& TextAreaWidget::GetDisplayText() const + { + return m_drawer.GetText(); + } + + inline float TextAreaWidget::GetCharacterSpacingOffset() const + { + return m_drawer.GetCharacterSpacingOffset(); + } + + inline float TextAreaWidget::GetLineSpacingOffset() const + { + return m_drawer.GetLineSpacingOffset(); + } + + inline const std::string& TextAreaWidget::GetText() const + { + return m_text; + } + + inline const Color& TextAreaWidget::GetTextColor() const + { + return m_drawer.GetColor(); + } + + inline const std::shared_ptr& TextAreaWidget::GetTextFont() const + { + return m_drawer.GetFont(); + } + + inline const Color& TextAreaWidget::GetTextOulineColor() const + { + return m_drawer.GetOutlineColor(); + } + + inline float TextAreaWidget::GetTextOulineThickness() const + { + return m_drawer.GetOutlineThickness(); + } + + inline TextStyleFlags TextAreaWidget::GetTextStyle() const + { + return m_drawer.GetStyle(); + } + + inline void TextAreaWidget::SetCharacterSize(unsigned int characterSize) + { + m_drawer.SetCharacterSize(characterSize); + + UpdateMinimumSize(); + UpdateDisplayText(); + } + + inline void TextAreaWidget::SetCharacterSpacingOffset(float offset) + { + m_drawer.SetCharacterSpacingOffset(offset); + + UpdateMinimumSize(); + UpdateDisplayText(); + } + + inline void TextAreaWidget::SetLineSpacingOffset(float offset) + { + m_drawer.SetLineSpacingOffset(offset); + + UpdateDisplayText(); + } + + inline void TextAreaWidget::SetText(const std::string& text) + { + m_text = text; + OnTextChanged(this, m_text); + + UpdateDisplayText(); + } + + inline void TextAreaWidget::SetTextColor(const Color& text) + { + m_drawer.SetColor(text); + + UpdateDisplayText(); + } + + inline void TextAreaWidget::SetTextFont(std::shared_ptr font) + { + m_drawer.SetFont(std::move(font)); + + UpdateDisplayText(); + } + + inline void TextAreaWidget::SetTextOutlineColor(const Color& color) + { + m_drawer.SetOutlineColor(color); + + UpdateDisplayText(); + } + + inline void TextAreaWidget::SetTextOutlineThickness(float thickness) + { + m_drawer.SetOutlineThickness(thickness); + + UpdateDisplayText(); + } + + inline void TextAreaWidget::SetTextStyle(TextStyleFlags style) + { + m_drawer.SetStyle(style); + + UpdateDisplayText(); + } +} + +#include diff --git a/src/Nazara/Core/StringExt.cpp b/src/Nazara/Core/StringExt.cpp index b776de5ae..1cb2df641 100644 --- a/src/Nazara/Core/StringExt.cpp +++ b/src/Nazara/Core/StringExt.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -91,6 +92,11 @@ namespace Nz }; } + std::size_t ComputeCharacterCount(const std::string_view& str) + { + return utf8::distance(str.data(), str.data() + str.size()); + } + std::string FromUtf16String(const std::u16string_view& u16str) { std::string result; @@ -112,6 +118,33 @@ namespace Nz return WideConverter::From(wstr.data(), wstr.size()); } + std::size_t GetCharacterPosition(const std::string_view& str, std::size_t characterIndex) + { + const char* ptr = str.data(); + const char* end = ptr + str.size(); + + try + { + utf8::advance(ptr, characterIndex, end); + + return ptr - str.data(); + } + catch (utf8::not_enough_room& /*e*/) + { + // Returns npos + } + catch (utf8::exception& e) + { + NazaraError("UTF-8 error: " + std::string(e.what())); + } + catch (std::exception& e) + { + NazaraError(e.what()); + } + + return std::string::npos; + } + std::string_view GetWord(const std::string_view& str, std::size_t wordIndex) { std::size_t pos = 0; diff --git a/src/Nazara/Widgets/AbstractTextAreaWidget.cpp b/src/Nazara/Widgets/AbstractTextAreaWidget.cpp new file mode 100644 index 000000000..f9552d59b --- /dev/null +++ b/src/Nazara/Widgets/AbstractTextAreaWidget.cpp @@ -0,0 +1,510 @@ +// Copyright (C) 2021 Jérôme "Lynix" Leclercq (lynix680@gmail.com) +// This file is part of the "Nazara Engine - Widgets module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Nz +{ + 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 = std::make_shared(Widgets::Instance()->GetTransparentMaterial()); + + auto& registry = GetRegistry(); + + m_textEntity = CreateEntity(); + + auto& gfxComponent = registry.emplace(m_textEntity, IsVisible()); + gfxComponent.AttachRenderable(m_textSprite, GetCanvas()->GetRenderMask()); + + auto& textNode = GetRegistry().emplace(m_textEntity); + textNode.SetParent(this); + textNode.SetPosition(paddingWidth, paddingHeight); + + SetCursor(SystemCursor::Text); + + EnableBackground(true); + } + + void AbstractTextAreaWidget::Clear() + { + AbstractTextDrawer& textDrawer = GetTextDrawer(); + textDrawer.Clear(); + UpdateTextSprite(); + + m_cursorPositionBegin.MakeZero(); + m_cursorPositionEnd.MakeZero(); + + RefreshCursor(); + } + + void AbstractTextAreaWidget::EnableLineWrap(bool enable) + { + if (m_isLineWrapEnabled != enable) + { + m_isLineWrapEnabled = enable; + + AbstractTextDrawer& textDrawer = GetTextDrawer(); + + if (enable) + textDrawer.SetMaxLineWidth(GetWidth()); + else + textDrawer.SetMaxLineWidth(std::numeric_limits::infinity()); + + UpdateTextSprite(); + } + } + + Vector2ui AbstractTextAreaWidget::GetHoveredGlyph(float x, float y) const + { + const AbstractTextDrawer& textDrawer = GetTextDrawer(); + + auto& textNode = GetRegistry().get(m_textEntity); + Vector2f textPosition = Vector2f(textNode.GetPosition(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) + { + 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) + { + Rectf bounds = textDrawer.GetGlyph(i).bounds; + if (x < bounds.x + bounds.width * 0.75f) + break; + } + + return Vector2ui(Vector2(i - firstLineGlyph, line)); + } + + return Vector2ui::Zero(); + } + + void AbstractTextAreaWidget::Layout() + { + BaseWidget::Layout(); + + if (m_isLineWrapEnabled) + { + AbstractTextDrawer& textDrawer = GetTextDrawer(); + + textDrawer.SetMaxLineWidth(GetWidth()); + UpdateTextSprite(); + } + + RefreshCursor(); + } + + bool AbstractTextAreaWidget::IsFocusable() const + { + return !m_readOnly; + } + + void AbstractTextAreaWidget::OnFocusLost() + { + // Hide cursors + auto& registry = GetRegistry(); + for (auto& cursor : m_cursors) + registry.get(cursor.entity).Hide(); + } + + void AbstractTextAreaWidget::OnFocusReceived() + { + if (!m_readOnly) + { + // Show cursors + auto& registry = GetRegistry(); + for (auto& cursor : m_cursors) + registry.get(cursor.entity).Show(); + } + } + + bool AbstractTextAreaWidget::OnKeyPressed(const WindowEvent::KeyEvent& key) + { + const AbstractTextDrawer& textDrawer = GetTextDrawer(); + + switch (key.virtualKey) + { + case Keyboard::VKey::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 Keyboard::VKey::Delete: + { + if (HasSelection()) + EraseSelection(); + else + Erase(GetGlyphIndex(m_cursorPositionBegin)); + + return true; + } + + case Keyboard::VKey::Down: + { + bool ignoreDefaultAction = false; + OnTextAreaKeyDown(this, &ignoreDefaultAction); + + if (ignoreDefaultAction) + return true; + + if (HasSelection()) + SetCursorPosition(m_cursorPositionEnd); + + MoveCursor({0, 1}); + return true; + } + + case Keyboard::VKey::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 Keyboard::VKey::Home: + { + bool ignoreDefaultAction = false; + OnTextAreaKeyHome(this, &ignoreDefaultAction); + + if (ignoreDefaultAction) + return true; + + SetCursorPosition({ 0U, key.control ? 0U : m_cursorPositionEnd.y }); + return true; + } + + case Keyboard::VKey::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 Keyboard::VKey::Return: + { + bool ignoreDefaultAction = false; + OnTextAreaKeyReturn(this, &ignoreDefaultAction); + + if (ignoreDefaultAction) + return true; + + if (!m_multiLineEnabled) + break; + + if (HasSelection()) + EraseSelection(); + + Write("\n"); + return true; + } + + case Keyboard::VKey::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 Keyboard::VKey::Up: + { + bool ignoreDefaultAction = false; + OnTextAreaKeyUp(this, &ignoreDefaultAction); + + if (ignoreDefaultAction) + return true; + + if (HasSelection()) + SetCursorPosition(m_cursorPositionBegin); + + MoveCursor({0, -1}); + return true; + } + + case Keyboard::VKey::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 WindowEvent::KeyEvent& /*key*/) + { + } + + void AbstractTextAreaWidget::OnMouseButtonPress(int x, int y, Mouse::Button button) + { + if (button == Mouse::Left) + { + SetFocus(); + + Vector2ui hoveredGlyph = GetHoveredGlyph(float(x), float(y)); + + // Shift extends selection + if (Keyboard::IsKeyPressed(Keyboard::VKey::LShift) || Keyboard::IsKeyPressed(Keyboard::VKey::RShift)) + SetSelection(hoveredGlyph, m_selectionCursor); + else + { + SetCursorPosition(hoveredGlyph); + m_selectionCursor = m_cursorPositionBegin; + } + + m_isMouseButtonDown = true; + } + } + + void AbstractTextAreaWidget::OnMouseButtonRelease(int, int, Mouse::Button button) + { + if (button == Mouse::Left) + m_isMouseButtonDown = false; + } + + void AbstractTextAreaWidget::OnMouseEnter() + { + if (!Mouse::IsButtonPressed(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 (Unicode::GetCategory(character) == Unicode::Category_Other_Control || (m_characterFilter && !m_characterFilter(character))) + return; + + if (HasSelection()) + EraseSelection(); + + Write(FromUtf32String(std::u32string_view(&character, 1))); + } + + void AbstractTextAreaWidget::RefreshCursor() + { + if (m_readOnly) + return; + + const AbstractTextDrawer& textDrawer = GetTextDrawer(); + + auto GetGlyph = [&](const Vector2ui& glyphPosition, std::size_t* glyphIndex) -> const 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; + }; + + auto& registry = GetRegistry(); + + // Move text so that cursor always lies in drawer bounds + const auto* lastGlyph = GetGlyph(m_cursorPositionEnd, nullptr); + float glyphPos = (lastGlyph) ? lastGlyph->bounds.x : 0.f; + float glyphWidth = (lastGlyph) ? lastGlyph->bounds.width : 0.f; + + auto& textNode = registry.get(m_textEntity); + float textPosition = textNode.GetPosition(CoordSys::Local).x - paddingWidth; + float cursorPosition = glyphPos + textPosition; + float width = GetWidth(); + + if (width <= textDrawer.GetBounds().width) + { + if (cursorPosition + glyphWidth > width) + textNode.Move(width - cursorPosition - glyphWidth, 0.f); + else if (cursorPosition - glyphWidth < 0.f) + textNode.Move(-cursorPosition + glyphWidth, 0.f); + } + else + textNode.Move(-textPosition, 0.f); //< Reset text position if we have enough room to show everything + + // Create/destroy cursor entities and sprites + std::size_t selectionLineCount = m_cursorPositionEnd.y - m_cursorPositionBegin.y + 1; + std::size_t oldSpriteCount = m_cursors.size(); + if (m_cursors.size() < selectionLineCount) + { + m_cursors.resize(selectionLineCount); + for (std::size_t i = oldSpriteCount; i < m_cursors.size(); ++i) + { + m_cursors[i].sprite = std::make_shared(Widgets::Instance()->GetTransparentMaterial()); + + m_cursors[i].entity = CreateEntity(); + registry.emplace(m_cursors[i].entity, HasFocus()).AttachRenderable(m_cursors[i].sprite); + registry.emplace(m_cursors[i].entity).SetParent(textNode); + } + } + else if (m_cursors.size() > selectionLineCount) + { + for (std::size_t i = selectionLineCount; i < m_cursors.size(); ++i) + DestroyEntity(m_cursors[i].entity); + + m_cursors.resize(selectionLineCount); + } + + // Resize every cursor sprite + for (unsigned int i = m_cursorPositionBegin.y; i <= m_cursorPositionEnd.y; ++i) + { + const auto& lineInfo = textDrawer.GetLine(i); + + auto& cursor = m_cursors[i - m_cursorPositionBegin.y]; + if (i == m_cursorPositionBegin.y || i == m_cursorPositionEnd.y) + { + // Partial selection (or no selection) + auto GetGlyphPos = [&](const 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); + + cursor.sprite->SetColor((m_cursorPositionBegin == m_cursorPositionEnd) ? Color::Black : Color(0, 0, 0, 50)); + cursor.sprite->SetSize(Vector2f(spriteSize, lineInfo.bounds.height)); + + registry.get(cursor.entity).SetPosition(beginX, lineInfo.bounds.y); + } + else + { + // Full line selection + cursor.sprite->SetColor(Color(0, 0, 0, 50)); + cursor.sprite->SetSize(Vector2f(lineInfo.bounds.width, lineInfo.bounds.height)); + + registry.get(cursor.entity).SetPosition(0.f, lineInfo.bounds.y); + } + } + } + + void AbstractTextAreaWidget::UpdateTextSprite() + { + m_textSprite->Update(GetTextDrawer()); + SetPreferredSize(Vector2f(m_textSprite->GetAABB().GetLengths())); + } +} diff --git a/src/Nazara/Widgets/RichTextAreaWidget.cpp b/src/Nazara/Widgets/RichTextAreaWidget.cpp new file mode 100644 index 000000000..dc9e2f0bb --- /dev/null +++ b/src/Nazara/Widgets/RichTextAreaWidget.cpp @@ -0,0 +1,198 @@ +// Copyright (C) 2021 Jérôme "Lynix" Leclercq (lynix680@gmail.com) +// This file is part of the "Nazara Engine - Widgets module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#include +#include + +namespace Nz +{ + RichTextAreaWidget::RichTextAreaWidget(BaseWidget* parent) : + AbstractTextAreaWidget(parent) + { + Layout(); + } + + void RichTextAreaWidget::AppendText(const std::string& text) + { + //m_text += text; + switch (m_echoMode) + { + case EchoMode::Normal: + m_drawer.AppendText(text); + break; + + case EchoMode::Hidden: + m_drawer.AppendText(std::string(ComputeCharacterCount(text), '*')); + break; + + case EchoMode::HiddenExceptLast: + { + // TODO + /*m_drawer.Clear(); + std::size_t textLength = m_text.GetLength(); + if (textLength >= 2) + { + std::size_t lastCharacterPosition = m_text.GetCharacterPosition(textLength - 2); + if (lastCharacterPosition != std::string::npos) + m_drawer.AppendText(std::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 > 0) ? lastGlyph - 1 : lastGlyph); + if (firstBlock == lastBlock) + { + const std::string& blockText = m_drawer.GetBlockText(firstBlock); + std::size_t blockFirstGlyph = m_drawer.GetBlockFirstGlyphIndex(firstBlock); + + std::string newText; + if (firstGlyph > blockFirstGlyph) + { + std::size_t characterPosition = GetCharacterPosition(blockText, firstGlyph - blockFirstGlyph); + NazaraAssert(characterPosition != std::string::npos, "Invalid character position"); + + newText.append(blockText.substr(0, characterPosition - 1)); + } + + if (lastGlyph < textLength) + newText.append(blockText.substr(GetCharacterPosition(blockText, lastGlyph - blockFirstGlyph))); + + if (!newText.empty()) + m_drawer.SetBlockText(firstBlock, std::move(newText)); + else + m_drawer.RemoveBlock(firstBlock); + } + else + { + const std::string& lastBlockText = m_drawer.GetBlockText(lastBlock); + std::size_t lastBlockGlyphIndex = m_drawer.GetBlockFirstGlyphIndex(lastBlock); + + // First, update/delete last block + std::size_t lastCharPos = GetCharacterPosition(lastBlockText, lastGlyph - lastBlockGlyphIndex); + if (lastCharPos != std::string::npos) + { + std::string newText = lastBlockText.substr(lastCharPos); + if (!newText.empty()) + m_drawer.SetBlockText(lastBlock, std::move(newText)); + else + m_drawer.RemoveBlock(lastBlock); + } + + // And then remove all middle blocks, remove in reverse order because of index shifting + assert(lastBlock > 0); + for (std::size_t i = lastBlock - 1; i > firstBlock; --i) + m_drawer.RemoveBlock(i); + + const std::string& firstBlockText = m_drawer.GetBlockText(firstBlock); + std::size_t firstBlockGlyphIndex = m_drawer.GetBlockFirstGlyphIndex(firstBlock); + + // And finally update/delete first block + if (firstGlyph > firstBlockGlyphIndex) + { + std::size_t firstCharPos = GetCharacterPosition(firstBlockText, firstGlyph - firstBlockGlyphIndex - 1); + if (firstCharPos != std::string::npos) + { + std::string newText = firstBlockText.substr(0, firstCharPos); + if (!newText.empty()) + m_drawer.SetBlockText(firstBlock, std::move(newText)); + else + m_drawer.RemoveBlock(firstBlock); + } + } + else + m_drawer.RemoveBlock(firstBlock); + } + + UpdateDisplayText(); + } + + void RichTextAreaWidget::Write(const std::string& text, std::size_t glyphPosition) + { + if (m_drawer.HasBlocks()) + { + auto block = m_drawer.GetBlock(m_drawer.FindBlock((glyphPosition > 0) ? glyphPosition - 1 : glyphPosition)); + std::size_t firstGlyph = block.GetFirstGlyphIndex(); + assert(glyphPosition >= firstGlyph); + + std::string blockText = block.GetText(); + std::size_t characterPosition = GetCharacterPosition(blockText, glyphPosition - firstGlyph); + blockText.insert(characterPosition, text); + + block.SetText(blockText); + } + else + m_drawer.AppendText(text); + + SetCursorPosition(glyphPosition + ComputeCharacterCount(text)); + + UpdateDisplayText(); + } + + AbstractTextDrawer& RichTextAreaWidget::GetTextDrawer() + { + return m_drawer; + } + + const 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(std::string(m_text.GetLength(), '*')); + break; + }*/ + + UpdateTextSprite(); + + SetCursorPosition(m_cursorPositionBegin); //< Refresh cursor position (prevent it from being outside of the text) + } +} diff --git a/src/Nazara/Widgets/TextAreaWidget.cpp b/src/Nazara/Widgets/TextAreaWidget.cpp new file mode 100644 index 000000000..c0a6a3115 --- /dev/null +++ b/src/Nazara/Widgets/TextAreaWidget.cpp @@ -0,0 +1,255 @@ +// Copyright (C) 2021 Jérôme "Lynix" Leclercq (lynix680@gmail.com) +// This file is part of the "Nazara Engine - Widgets module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#include +#include +#include +#include +#include +#include + +namespace Nz +{ + TextAreaWidget::TextAreaWidget(BaseWidget* parent) : + AbstractTextAreaWidget(parent) + { + SetCharacterSize(GetCharacterSize()); //< Actualize minimum / preferred size + + Layout(); + } + + void TextAreaWidget::AppendText(const std::string& text) + { + m_text += text; + + switch (m_echoMode) + { + case EchoMode::Normal: + m_drawer.AppendText(text); + break; + + case EchoMode::Hidden: + m_drawer.AppendText(std::string(ComputeCharacterCount(text), '*')); + break; + + case EchoMode::HiddenExceptLast: + { + m_drawer.Clear(); + std::size_t textLength = ComputeCharacterCount(m_text); + if (textLength >= 2) + { + std::size_t lastCharacterPosition = GetCharacterPosition(m_text, textLength - 2); + if (lastCharacterPosition != std::string::npos) + m_drawer.AppendText(std::string(textLength - 1, '*')); + } + + if (textLength >= 1) + m_drawer.AppendText(m_text.substr(GetCharacterPosition(m_text, textLength - 1))); //< FIXME: getting last character position could be massively optimized + + break; + } + } + + UpdateTextSprite(); + + OnTextChanged(this, m_text); + } + + void TextAreaWidget::Clear() + { + AbstractTextAreaWidget::Clear(); + + m_text.clear(); + OnTextChanged(this, m_text); + } + + void TextAreaWidget::Erase(std::size_t firstGlyph, std::size_t lastGlyph) + { + if (firstGlyph > lastGlyph) + std::swap(firstGlyph, lastGlyph); + + std::size_t textLength = ComputeCharacterCount(m_text); + if (firstGlyph > textLength) + return; + + std::string newText; + if (firstGlyph > 0) + { + std::size_t characterPosition = GetCharacterPosition(m_text, firstGlyph); + NazaraAssert(characterPosition != std::string::npos, "Invalid character position"); + + newText.append(m_text.substr(0, characterPosition)); + } + + if (lastGlyph < textLength) + { + std::size_t characterPosition = GetCharacterPosition(m_text, lastGlyph); + NazaraAssert(characterPosition != std::string::npos, "Invalid character position"); + + newText.append(m_text.substr(characterPosition)); + } + + SetText(newText); + } + + void TextAreaWidget::Write(const std::string& text, std::size_t glyphPosition) + { + if (glyphPosition >= m_drawer.GetGlyphCount()) + { + // It's faster to append than to insert in the middle + AppendText(text); + SetCursorPosition(m_drawer.GetGlyphCount()); + } + else + { + m_text.insert(GetCharacterPosition(m_text, glyphPosition), text); + SetText(m_text); + + SetCursorPosition(glyphPosition + ComputeCharacterCount(text)); + } + } + + AbstractTextDrawer& TextAreaWidget::GetTextDrawer() + { + return m_drawer; + } + + const AbstractTextDrawer& TextAreaWidget::GetTextDrawer() const + { + return m_drawer; + } + + void TextAreaWidget::HandleIndentation(bool add) + { + if (add) + Write("\t"); + else + { + std::size_t currentGlyph = GetGlyphIndex(m_cursorPositionBegin); + + if (currentGlyph > 0 && m_text[GetCharacterPosition(m_text, currentGlyph - 1U)] == '\t') // Check if previous glyph is a tab + { + Erase(currentGlyph - 1U); + + if (m_cursorPositionBegin.x < static_cast(m_drawer.GetLineGlyphCount(m_cursorPositionBegin.y))) + MoveCursor(-1); + } + } + } + + void TextAreaWidget::HandleSelectionIndentation(bool add) + { + for (unsigned line = m_cursorPositionBegin.y; line <= m_cursorPositionEnd.y; ++line) + { + const Vector2ui cursorPositionBegin = m_cursorPositionBegin; + const Vector2ui cursorPositionEnd = m_cursorPositionEnd; + + if (add) + { + Write("\t", {0U, line}); + SetSelection(cursorPositionBegin + (cursorPositionBegin.y == line && cursorPositionBegin.x != 0U ? Vector2ui{ 1U, 0U } : Vector2ui{}), + cursorPositionEnd + (cursorPositionEnd.y == line ? Vector2ui{ 1U, 0U } : Vector2ui{})); + } + else + { + if (m_drawer.GetLineGlyphCount(line) == 0) + continue; + + std::size_t firstGlyph = GetGlyphIndex({ 0U, line }); + + if (m_text[GetCharacterPosition(m_text, firstGlyph)] == '\t') + { + Erase(firstGlyph); + SetSelection(cursorPositionBegin - (cursorPositionBegin.y == line && cursorPositionBegin.x != 0U ? Vector2ui{ 1U, 0U } : Vector2ui{}), + cursorPositionEnd - (cursorPositionEnd.y == line && cursorPositionEnd.x != 0U ? Vector2ui{ 1U, 0U } : Vector2ui{})); + } + } + } + } + + void TextAreaWidget::HandleWordCursorMove(bool left) + { + if (left) + { + std::size_t index = GetGlyphIndex(m_cursorPositionBegin); + if (index == 0) + return; + + std::size_t spaceIndex = m_text.rfind(' ', index - 2); + std::size_t endlIndex = m_text.rfind('\n', index - 1); + + if ((spaceIndex > endlIndex || endlIndex == std::string::npos) && spaceIndex != std::string::npos) + SetCursorPosition(spaceIndex + 1); + else if (endlIndex != std::string::npos) + { + if (index == endlIndex + 1) + SetCursorPosition(endlIndex); + else + SetCursorPosition(endlIndex + 1); + } + else + SetCursorPosition({ 0U, m_cursorPositionBegin.y }); + } + else + { + std::size_t index = GetGlyphIndex(m_cursorPositionEnd); + std::size_t spaceIndex = m_text.find(' ', index); + std::size_t endlIndex = m_text.find('\n', index); + + if (spaceIndex < endlIndex && spaceIndex != std::string::npos) + { + if (ComputeCharacterCount(m_text) > spaceIndex) + SetCursorPosition(spaceIndex + 1); + else + SetCursorPosition({ static_cast(m_drawer.GetLineGlyphCount(m_cursorPositionEnd.y)), m_cursorPositionEnd.y }); + } + else if (endlIndex != std::string::npos) + { + if (index == endlIndex) + SetCursorPosition(endlIndex + 1); + else + SetCursorPosition(endlIndex); + } + else + SetCursorPosition({ static_cast(m_drawer.GetLineGlyphCount(m_cursorPositionEnd.y)), m_cursorPositionEnd.y }); + } + } + + void TextAreaWidget::UpdateDisplayText() + { + switch (m_echoMode) + { + case EchoMode::Normal: + m_drawer.SetText(m_text); + break; + + case EchoMode::Hidden: + case EchoMode::HiddenExceptLast: + m_drawer.SetText(std::string(ComputeCharacterCount(m_text), '*')); + break; + } + + UpdateTextSprite(); + + SetCursorPosition(m_cursorPositionBegin); //< Refresh cursor position (prevent it from being outside of the text) + } + + void TextAreaWidget::UpdateMinimumSize() + { + std::size_t fontCount = m_drawer.GetFontCount(); + float lineHeight = 0; + int spaceAdvance = 0; + for (std::size_t i = 0; i < fontCount; ++i) + { + const std::shared_ptr& font = m_drawer.GetFont(i); + + const Font::SizeInfo& sizeInfo = font->GetSizeInfo(m_drawer.GetCharacterSize()); + lineHeight = std::max(lineHeight, m_drawer.GetLineHeight()); + spaceAdvance = std::max(spaceAdvance, sizeInfo.spaceAdvance); + } + + Vector2f size = { float(spaceAdvance), lineHeight + 5.f }; + SetMinimumSize(size); + } +}