From 8013bd5d3b675b13283b382b6b94909daec4cc4f Mon Sep 17 00:00:00 2001 From: Lynix Date: Tue, 15 Mar 2016 23:02:00 +0100 Subject: [PATCH 1/8] Add current work Former-commit-id: 7f1e46e484edbbfd120a7a67ea1c36bee460e517 --- include/Nazara/Utility/RichTextDrawer.hpp | 140 ++++++++++++++++++++++ include/Nazara/Utility/RichTextDrawer.inl | 133 ++++++++++++++++++++ 2 files changed, 273 insertions(+) create mode 100644 include/Nazara/Utility/RichTextDrawer.hpp create mode 100644 include/Nazara/Utility/RichTextDrawer.inl diff --git a/include/Nazara/Utility/RichTextDrawer.hpp b/include/Nazara/Utility/RichTextDrawer.hpp new file mode 100644 index 000000000..65375c13c --- /dev/null +++ b/include/Nazara/Utility/RichTextDrawer.hpp @@ -0,0 +1,140 @@ +// Copyright (C) 2016 Jérôme Leclercq +// This file is part of the "Nazara Engine - Utility module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#pragma once + +#ifndef NAZARA_RICHTEXTDRAWER_HPP +#define NAZARA_RICHTEXTDRAWER_HPP + +#include +#include +#include +#include +#include +#include + +namespace Nz +{ + class NAZARA_UTILITY_API RichTextDrawer : public AbstractTextDrawer + { + public: + class BlockRef; + + RichTextDrawer(); + RichTextDrawer(const RichTextDrawer& drawer); + RichTextDrawer(RichTextDrawer&& drawer); + virtual ~RichTextDrawer(); + + BlockRef Append(const String& str); + + void Clear(); + + unsigned int GetBlockCharacterSize(std::size_t index) const; + const Color& GetBlockColor(std::size_t index) const; + std::size_t GetBlockCount() const; + const FontRef& GetBlockFont(std::size_t index) const; + UInt32 GetBlockStyle(std::size_t index) const; + const String& GetBlockText(std::size_t index) const; + + unsigned int GetDefaultCharacterSize() const; + const Color& GetDefaultColor() const; + const FontRef& GetDefaultFont() const; + UInt32 GetDefaultStyle() const; + + const Rectui& GetBounds() const override; + Font* GetFont(unsigned int index) const override; + unsigned int GetFontCount() const override; + const Glyph& GetGlyph(unsigned int index) const override; + unsigned int GetGlyphCount() const override; + + void MergeBlocks(); + + void RemoveBlock(std::size_t index); + + void SetBlockCharacterSize(std::size_t index, unsigned int characterSize); + void SetBlockColor(std::size_t index, const Color& color); + void SetBlockFont(std::size_t index, FontRef font); + void SetBlockStyle(std::size_t index, UInt32 style); + void SetBlockText(std::size_t index, const String& str); + + void SetDefaultCharacterSize(unsigned int characterSize); + void SetDefaultColor(const Color& color); + void SetDefaultFont(FontRef font); + void SetDefaultStyle(UInt32 style); + + RichTextDrawer& operator=(const RichTextDrawer& drawer); + RichTextDrawer& operator=(RichTextDrawer&& drawer); + + static RichTextDrawer Draw(const String& str, unsigned int characterSize, UInt32 style = TextStyle_Regular, const Color& color = Color::White); + static RichTextDrawer Draw(Font* font, const String& str, unsigned int characterSize, UInt32 style = TextStyle_Regular, const Color& color = Color::White); + + private: + void ClearGlyphs() const; + void ConnectFontSlots(); + void DisconnectFontSlots(); + void GenerateGlyphs(const String& text) const; + void OnFontAtlasLayerChanged(const Font* font, AbstractImage* oldLayer, AbstractImage* newLayer); + void OnFontInvalidated(const Font* font); + void OnFontRelease(const Font* object); + void UpdateGlyphs() const; + + NazaraSlot(Font, OnFontAtlasChanged, m_atlasChangedSlot); + NazaraSlot(Font, OnFontAtlasLayerChanged, m_atlasLayerChangedSlot); + NazaraSlot(Font, OnFontGlyphCacheCleared, m_glyphCacheClearedSlot); + NazaraSlot(Font, OnFontRelease, m_fontReleaseSlot); + + struct Block + { + Color color; + String text; + UInt32 style; + unsigned int characterSize; + unsigned int fontIndex; + }; + + Color m_defaultColor; + FontRef m_defaultFont; + UInt32 m_defaultStyle; + unsigned int m_defaultCharacterSize; + std::unordered_map m_fonts; + mutable std::vector m_glyphs; + mutable Rectf m_workingBounds; + mutable Rectui m_bounds; + mutable Vector2ui m_drawPos; + mutable bool m_glyphUpdated; + }; + + class RichTextDrawer::BlockRef + { + public: + BlockRef(const BlockRef&) = default; + BlockRef(BlockRef&&) = default; + ~BlockRef() = default; + + inline unsigned int GetCharacterSize() const; + inline Color GetColor() const; + inline const FontRef& GetFont() const; + inline UInt32 GetStyle() const; + inline const String& GetText() const; + + inline void SetCharacterSize(unsigned int size); + inline void SetColor(Color color); + inline void SetFont(FontRef font); + inline void SetStyle(UInt32 style); + inline void SetText(const String& text); + + BlockRef& operator=(const BlockRef&) = default; + BlockRef& operator=(BlockRef&&) = default; + + private: + inline BlockRef(RichTextDrawer& drawer, std::size_t index); + + std::size_t m_blockIndex; + RichTextDrawer& m_drawer; + }; +} + +#include + +#endif // NAZARA_RICHTEXTDRAWER_HPP diff --git a/include/Nazara/Utility/RichTextDrawer.inl b/include/Nazara/Utility/RichTextDrawer.inl new file mode 100644 index 000000000..5af95ec84 --- /dev/null +++ b/include/Nazara/Utility/RichTextDrawer.inl @@ -0,0 +1,133 @@ +// Copyright (C) 2016 Jérôme Leclercq +// This file is part of the "Nazara Engine - Utility module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#include + +namespace Nz +{ + /*! + * \class Nz::RichTextDrawer::BlockRef + * \brief Helper class representing a block inside a RichTextDrawer, allowing easier access. + * + * \warning This class is meant for temporary use, moving or destroying the RichTextDrawer or one of its blocks invalidates all BlockRef + */ + + inline RichTextDrawer::BlockRef::BlockRef(RichTextDrawer& drawer, std::size_t index) : + m_blockIndex(index), + m_drawer(drawer) + { + } + + /*! + * Returns the character size used for the characters of the referenced block + * \return The referenced block character size + * + * \see GetColor, GetFont, GetStyle, GetText, SetCharacterSize + */ + inline unsigned int RichTextDrawer::BlockRef::GetCharacterSize() const + { + return m_drawer.GetBlockCharacterSize(m_blockIndex); + } + + /*! + * Returns the color used for the characters of the referenced block + * \return The referenced block color + * + * \see GetCharacterSize, GetFont, GetStyle, GetText, SetColor + */ + inline Color RichTextDrawer::BlockRef::GetColor() const + { + return m_drawer.GetBlockColor(m_blockIndex); + } + + /*! + * Returns the font used for the characters of the referenced block + * \return A reference on the referenced block font + * + * \see GetCharacterSize, GetColor, GetStyle, GetText, SetFont + */ + inline const FontRef& RichTextDrawer::BlockRef::GetFont() const + { + return m_drawer.GetBlockFont(m_blockIndex); + } + + /*! + * Returns the style flags used for the characters of the referenced block + * \return The referenced block style flags (see TextStyleFlags) + * + * \see GetCharacterSize, GetColor, GetFont, GetText, SetStyle + */ + inline UInt32 RichTextDrawer::BlockRef::GetStyle() const + { + return m_drawer.GetBlockStyle(m_blockIndex); + } + + /*! + * Returns the text of the referenced block + * \return The referenced block text + * + * \see GetCharacterSize, GetColor, GetFont, GetStyle, SetText + */ + inline const String& RichTextDrawer::BlockRef::GetText() const + { + return m_drawer.GetBlockText(m_blockIndex); + } + + /*! + * Changes the character size of the referenced block characters + * \remark This invalidates the drawer and will force a (complete or partial, depending on the block index) glyph regeneration to occur. + * + * \see GetCharacterSize, SetColor, SetFont, SetStyle, SetText + */ + inline void RichTextDrawer::BlockRef::SetCharacterSize(unsigned int size) + { + m_drawer.SetBlockCharacterSize(m_blockIndex, size); + } + + /*! + * Changes the color of the referenced block characters + * \remark This is the only property that can be changed without forcing a glyph regeneration + * + * \see GetColor, SetCharacterSize, SetFont, SetStyle, SetText + */ + inline void RichTextDrawer::BlockRef::SetColor(Color color) + { + m_drawer.SetBlockColor(m_blockIndex, color); + } + + /*! + * Changes the font of the referenced block characters + * \remark This invalidates the drawer and will force a (complete or partial, depending on the block index) glyph regeneration to occur. + * + * \see GetCharacterSize, SetCharacterSize, SetColor, SetStyle, SetText + */ + inline void RichTextDrawer::BlockRef::SetFont(FontRef font) + { + m_drawer.SetBlockFont(m_blockIndex, std::move(font)); + } + + /*! + * Changes the style flags of the referenced block characters + * \remark This invalidates the drawer and will force a (complete or partial, depending on the block index) glyph regeneration to occur. + * + * \see GetStyle, SetCharacterSize, SetColor, SetFont, SetText + */ + inline void RichTextDrawer::BlockRef::SetStyle(UInt32 style) + { + m_drawer.SetBlockStyle(m_blockIndex, style); + } + + /*! + * Changes the text of the referenced block + * \remark This invalidates the drawer and will force a (complete or partial, depending on the block index) glyph regeneration to occur. + * + * \see GetText, SetCharacterSize, SetColor, SetFont, SetStyle + */ + inline void RichTextDrawer::BlockRef::SetText(const String& text) + { + m_drawer.SetBlockText(m_blockIndex, text); + } +} + +#include From 5504dd59fbd8eeae136394b2cc61fa86ca2a2338 Mon Sep 17 00:00:00 2001 From: Lynix Date: Fri, 1 Sep 2017 08:58:42 +0200 Subject: [PATCH 2/8] Utility/RichTextDrawer: Update header --- include/Nazara/Utility/RichTextDrawer.hpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/include/Nazara/Utility/RichTextDrawer.hpp b/include/Nazara/Utility/RichTextDrawer.hpp index 65375c13c..b3154b740 100644 --- a/include/Nazara/Utility/RichTextDrawer.hpp +++ b/include/Nazara/Utility/RichTextDrawer.hpp @@ -42,11 +42,13 @@ namespace Nz const FontRef& GetDefaultFont() const; UInt32 GetDefaultStyle() const; - const Rectui& GetBounds() const override; - Font* GetFont(unsigned int index) const override; - unsigned int GetFontCount() const override; - const Glyph& GetGlyph(unsigned int index) const override; - unsigned int GetGlyphCount() const override; + 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; void MergeBlocks(); @@ -98,6 +100,7 @@ namespace Nz UInt32 m_defaultStyle; unsigned int m_defaultCharacterSize; std::unordered_map m_fonts; + std::vector m_blocks; mutable std::vector m_glyphs; mutable Rectf m_workingBounds; mutable Rectui m_bounds; From 6bc99a6d5eb4ca3aff1933befd8b5d90b02cc10c Mon Sep 17 00:00:00 2001 From: Lynix Date: Tue, 9 Jul 2019 22:50:22 +0200 Subject: [PATCH 3/8] Utility/SimpleTextDrawer: Fix reserve --- src/Nazara/Utility/SimpleTextDrawer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Nazara/Utility/SimpleTextDrawer.cpp b/src/Nazara/Utility/SimpleTextDrawer.cpp index 6bf0830df..aca74968d 100644 --- a/src/Nazara/Utility/SimpleTextDrawer.cpp +++ b/src/Nazara/Utility/SimpleTextDrawer.cpp @@ -452,7 +452,7 @@ namespace Nz const Font::SizeInfo& sizeInfo = m_font->GetSizeInfo(m_characterSize); - m_glyphs.reserve(m_glyphs.size() + characters.size() * (m_outlineThickness > 0.f) ? 2 : 1); + m_glyphs.reserve(m_glyphs.size() + characters.size() * ((m_outlineThickness > 0.f) ? 2 : 1)); for (char32_t character : characters) { if (m_previousCharacter != 0) From 451b3de69c1421447b033b8f326989d06c716cce Mon Sep 17 00:00:00 2001 From: Lynix Date: Tue, 9 Jul 2019 22:53:06 +0200 Subject: [PATCH 4/8] Utility: RichTextDrawer now works (WIP) --- include/Nazara/Utility/RichTextDrawer.hpp | 104 +++-- include/Nazara/Utility/RichTextDrawer.inl | 232 ++++++++++- src/Nazara/Utility/RichTextDrawer.cpp | 476 ++++++++++++++++++++++ 3 files changed, 768 insertions(+), 44 deletions(-) create mode 100644 src/Nazara/Utility/RichTextDrawer.cpp diff --git a/include/Nazara/Utility/RichTextDrawer.hpp b/include/Nazara/Utility/RichTextDrawer.hpp index b3154b740..3a681e20d 100644 --- a/include/Nazara/Utility/RichTextDrawer.hpp +++ b/include/Nazara/Utility/RichTextDrawer.hpp @@ -7,7 +7,7 @@ #ifndef NAZARA_RICHTEXTDRAWER_HPP #define NAZARA_RICHTEXTDRAWER_HPP -#include +#include #include #include #include @@ -24,23 +24,23 @@ namespace Nz RichTextDrawer(); RichTextDrawer(const RichTextDrawer& drawer); RichTextDrawer(RichTextDrawer&& drawer); - virtual ~RichTextDrawer(); + ~RichTextDrawer(); - BlockRef Append(const String& str); + BlockRef AppendText(const String& str); - void Clear(); + inline void Clear(); - unsigned int GetBlockCharacterSize(std::size_t index) const; - const Color& GetBlockColor(std::size_t index) const; - std::size_t GetBlockCount() const; - const FontRef& GetBlockFont(std::size_t index) const; - UInt32 GetBlockStyle(std::size_t index) const; - const String& GetBlockText(std::size_t index) 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 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; - unsigned int GetDefaultCharacterSize() const; - const Color& GetDefaultColor() const; - const FontRef& GetDefaultFont() const; - UInt32 GetDefaultStyle() const; + 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; @@ -54,62 +54,82 @@ namespace Nz void RemoveBlock(std::size_t index); - void SetBlockCharacterSize(std::size_t index, unsigned int characterSize); - void SetBlockColor(std::size_t index, const Color& color); - void SetBlockFont(std::size_t index, FontRef font); - void SetBlockStyle(std::size_t index, UInt32 style); - void SetBlockText(std::size_t index, const String& str); + inline void SetBlockCharacterSize(std::size_t index, unsigned int characterSize); + inline void SetBlockColor(std::size_t index, const Color& color); + inline void SetBlockFont(std::size_t index, FontRef font); + inline void SetBlockStyle(std::size_t index, TextStyleFlags style); + inline void SetBlockText(std::size_t index, const String& str); - void SetDefaultCharacterSize(unsigned int characterSize); - void SetDefaultColor(const Color& color); - void SetDefaultFont(FontRef font); - void SetDefaultStyle(UInt32 style); + inline void SetDefaultCharacterSize(unsigned int characterSize); + inline void SetDefaultColor(const Color& color); + inline void SetDefaultFont(const FontRef& font); + inline void SetDefaultStyle(TextStyleFlags style); RichTextDrawer& operator=(const RichTextDrawer& drawer); RichTextDrawer& operator=(RichTextDrawer&& drawer); - static RichTextDrawer Draw(const String& str, unsigned int characterSize, UInt32 style = TextStyle_Regular, const Color& color = Color::White); - static RichTextDrawer Draw(Font* font, const String& str, unsigned int characterSize, UInt32 style = TextStyle_Regular, const Color& color = Color::White); + //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); private: - void ClearGlyphs() const; - void ConnectFontSlots(); - void DisconnectFontSlots(); - void GenerateGlyphs(const String& text) const; + struct Block; + + inline void AppendNewLine(const Font* font, unsigned int characterSize) const; + inline void ClearGlyphs() const; + inline void ConnectFontSlots(); + inline void DisconnectFontSlots(); + bool GenerateGlyph(Glyph& glyph, char32_t character, float outlineThickness, bool lineWrap, const Font* font, const Color& color, TextStyleFlags style, unsigned int characterSize, int renderOrder, int* advance) const; + void GenerateGlyphs(const Font* font, const Color& color, TextStyleFlags style, unsigned int characterSize, const Color& outlineColor, float outlineThickness, const String& text) const; + inline std::size_t HandleFontAddition(const FontRef& font); + inline void ReleaseFont(std::size_t fontIndex); + + inline void InvalidateGlyphs(); + void OnFontAtlasLayerChanged(const Font* font, AbstractImage* oldLayer, AbstractImage* newLayer); void OnFontInvalidated(const Font* font); void OnFontRelease(const Font* object); - void UpdateGlyphs() const; - NazaraSlot(Font, OnFontAtlasChanged, m_atlasChangedSlot); - NazaraSlot(Font, OnFontAtlasLayerChanged, m_atlasLayerChangedSlot); - NazaraSlot(Font, OnFontGlyphCacheCleared, m_glyphCacheClearedSlot); - NazaraSlot(Font, OnFontRelease, m_fontReleaseSlot); + void UpdateGlyphs() const; struct Block { + std::size_t fontIndex; Color color; String text; - UInt32 style; + TextStyleFlags style; unsigned int characterSize; - unsigned int fontIndex; + }; + + struct FontData + { + FontRef font; + std::size_t useCount = 0; + + NazaraSlot(Font, OnFontAtlasChanged, atlasChangedSlot); + NazaraSlot(Font, OnFontAtlasLayerChanged, atlasLayerChangedSlot); + NazaraSlot(Font, OnFontGlyphCacheCleared, glyphCacheClearedSlot); + NazaraSlot(Font, OnFontRelease, fontReleaseSlot); }; Color m_defaultColor; + TextStyleFlags m_defaultStyle; FontRef m_defaultFont; - UInt32 m_defaultStyle; - unsigned int m_defaultCharacterSize; - std::unordered_map m_fonts; + std::unordered_map m_fontIndexes; std::vector m_blocks; + std::vector m_fonts; mutable std::vector m_glyphs; + mutable std::vector m_lines; mutable Rectf m_workingBounds; - mutable Rectui m_bounds; + mutable Recti m_bounds; mutable Vector2ui m_drawPos; mutable bool m_glyphUpdated; + unsigned int m_defaultCharacterSize; }; class RichTextDrawer::BlockRef { + friend RichTextDrawer; + public: BlockRef(const BlockRef&) = default; BlockRef(BlockRef&&) = default; @@ -118,13 +138,13 @@ namespace Nz inline unsigned int GetCharacterSize() const; inline Color GetColor() const; inline const FontRef& GetFont() const; - inline UInt32 GetStyle() const; + inline TextStyleFlags GetStyle() const; inline const String& GetText() const; inline void SetCharacterSize(unsigned int size); inline void SetColor(Color color); inline void SetFont(FontRef font); - inline void SetStyle(UInt32 style); + inline void SetStyle(TextStyleFlags style); inline void SetText(const String& text); BlockRef& operator=(const BlockRef&) = default; diff --git a/include/Nazara/Utility/RichTextDrawer.inl b/include/Nazara/Utility/RichTextDrawer.inl index 5af95ec84..c2591a20b 100644 --- a/include/Nazara/Utility/RichTextDrawer.inl +++ b/include/Nazara/Utility/RichTextDrawer.inl @@ -2,10 +2,238 @@ // This file is part of the "Nazara Engine - Utility module" // For conditions of distribution and use, see copyright notice in Config.hpp +#include #include namespace Nz { + inline void RichTextDrawer::Clear() + { + m_fontIndexes.clear(); + m_blocks.clear(); + m_fonts.clear(); + m_glyphs.clear(); + ClearGlyphs(); + } + + inline unsigned int RichTextDrawer::GetBlockCharacterSize(std::size_t index) const + { + NazaraAssert(index < m_blocks.size(), "Invalid block index"); + return m_blocks[index].characterSize; + } + + inline const Color& RichTextDrawer::GetBlockColor(std::size_t index) const + { + NazaraAssert(index < m_blocks.size(), "Invalid block index"); + return m_blocks[index].color; + } + + inline std::size_t RichTextDrawer::GetBlockCount() const + { + return m_blocks.size(); + } + + inline const FontRef& RichTextDrawer::GetBlockFont(std::size_t index) const + { + NazaraAssert(index < m_blocks.size(), "Invalid block index"); + std::size_t fontIndex = m_blocks[index].fontIndex; + assert(fontIndex < m_fonts.size()); + return m_fonts[fontIndex].font; + } + + inline TextStyleFlags RichTextDrawer::GetBlockStyle(std::size_t index) const + { + NazaraAssert(index < m_blocks.size(), "Invalid block index"); + return m_blocks[index].style; + } + + inline const String& RichTextDrawer::GetBlockText(std::size_t index) const + { + NazaraAssert(index < m_blocks.size(), "Invalid block index"); + return m_blocks[index].text; + } + + inline unsigned int RichTextDrawer::GetDefaultCharacterSize() const + { + return m_defaultCharacterSize; + } + + inline const Color& RichTextDrawer::GetDefaultColor() const + { + return m_defaultColor; + } + + inline const FontRef& RichTextDrawer::GetDefaultFont() const + { + return m_defaultFont; + } + + inline TextStyleFlags RichTextDrawer::GetDefaultStyle() const + { + return m_defaultStyle; + } + + inline void RichTextDrawer::AppendNewLine(const Font* font, unsigned int characterSize) const + { + // Ensure we're appending from last line + Line& lastLine = m_lines.back(); + + const Font::SizeInfo& sizeInfo = font->GetSizeInfo(characterSize); + + unsigned int previousDrawPos = m_drawPos.x; + + // Reset cursor + m_drawPos.x = 0; + m_drawPos.y += sizeInfo.lineHeight; + + m_workingBounds.ExtendTo(lastLine.bounds); + m_lines.emplace_back(Line{ Rectf(0.f, float(sizeInfo.lineHeight * m_lines.size()), 0.f, float(sizeInfo.lineHeight)), m_glyphs.size() + 1 }); + } + + inline void RichTextDrawer::ClearGlyphs() const + { + m_bounds.MakeZero(); + m_lines.clear(); + m_glyphs.clear(); + m_glyphUpdated = true; + m_workingBounds.MakeZero(); //< Compute bounds as float to speedup bounds computation (as casting between floats and integers is costly) + } + + inline void RichTextDrawer::ConnectFontSlots() + { + for (auto& fontData : m_fonts) + { + fontData.atlasChangedSlot.Connect(fontData.font->OnFontAtlasChanged, this, &RichTextDrawer::OnFontInvalidated); + fontData.atlasLayerChangedSlot.Connect(fontData.font->OnFontAtlasLayerChanged, this, &RichTextDrawer::OnFontAtlasLayerChanged); + fontData.fontReleaseSlot.Connect(fontData.font->OnFontDestroy, this, &RichTextDrawer::OnFontRelease); + fontData.glyphCacheClearedSlot.Connect(fontData.font->OnFontGlyphCacheCleared, this, &RichTextDrawer::OnFontInvalidated); + } + } + + inline void RichTextDrawer::DisconnectFontSlots() + { + for (auto& fontData : m_fonts) + { + fontData.atlasChangedSlot.Disconnect(); + fontData.atlasLayerChangedSlot.Disconnect(); + fontData.fontReleaseSlot.Disconnect(); + fontData.glyphCacheClearedSlot.Disconnect(); + } + } + + inline std::size_t RichTextDrawer::HandleFontAddition(const FontRef& font) + { + auto it = m_fontIndexes.find(font); + if (it == m_fontIndexes.end()) + { + std::size_t fontIndex = m_fonts.size(); + m_fonts.emplace_back(); + auto& fontData = m_fonts.back(); + fontData.font = font; + fontData.atlasChangedSlot.Connect(font->OnFontAtlasChanged, this, &RichTextDrawer::OnFontInvalidated); + fontData.atlasLayerChangedSlot.Connect(font->OnFontAtlasLayerChanged, this, &RichTextDrawer::OnFontAtlasLayerChanged); + fontData.fontReleaseSlot.Connect(font->OnFontDestroy, this, &RichTextDrawer::OnFontRelease); + fontData.glyphCacheClearedSlot.Connect(font->OnFontGlyphCacheCleared, this, &RichTextDrawer::OnFontInvalidated); + + it = m_fontIndexes.emplace(font, fontIndex).first; + } + + return it->second; + } + + inline void RichTextDrawer::ReleaseFont(std::size_t fontIndex) + { + assert(fontIndex < m_fonts.size()); + + FontData& fontData = m_fonts[fontIndex]; + assert(fontData.useCount > 0); + + if (--fontData.useCount == 0) + { + // Shift font indexes + m_fontIndexes.erase(fontData.font); + for (auto it = m_fontIndexes.begin(); it != m_fontIndexes.end(); ++it) + { + if (it->second > fontIndex) + it->second--; + } + + m_fonts.erase(m_fonts.begin() + fontIndex); + } + } + + inline void RichTextDrawer::SetBlockCharacterSize(std::size_t index, unsigned int characterSize) + { + NazaraAssert(index < m_blocks.size(), "Invalid block index"); + m_blocks[index].characterSize = characterSize; + + InvalidateGlyphs(); + } + + inline void RichTextDrawer::SetBlockColor(std::size_t index, const Color& color) + { + NazaraAssert(index < m_blocks.size(), "Invalid block index"); + m_blocks[index].color = color; + + InvalidateGlyphs(); + } + + inline void RichTextDrawer::SetBlockFont(std::size_t index, FontRef font) + { + NazaraAssert(index < m_blocks.size(), "Invalid block index"); + std::size_t fontIndex = HandleFontAddition(font); + std::size_t oldFontIndex = m_blocks[index].fontIndex; + + if (oldFontIndex != fontIndex) + { + ReleaseFont(oldFontIndex); + + m_fonts[fontIndex].useCount++; + m_blocks[index].fontIndex = fontIndex; + } + } + + inline void RichTextDrawer::SetBlockStyle(std::size_t index, TextStyleFlags style) + { + NazaraAssert(index < m_blocks.size(), "Invalid block index"); + m_blocks[index].style = style; + + InvalidateGlyphs(); + } + + inline void RichTextDrawer::SetBlockText(std::size_t index, const String& str) + { + NazaraAssert(index < m_blocks.size(), "Invalid block index"); + m_blocks[index].text = str; + + InvalidateGlyphs(); + } + + inline void RichTextDrawer::SetDefaultCharacterSize(unsigned int characterSize) + { + m_defaultCharacterSize = characterSize; + } + + inline void RichTextDrawer::SetDefaultColor(const Color& color) + { + m_defaultColor = color; + } + + inline void RichTextDrawer::SetDefaultFont(const FontRef& font) + { + m_defaultFont = font; + } + + inline void RichTextDrawer::SetDefaultStyle(TextStyleFlags style) + { + m_defaultStyle = style; + } + + inline void RichTextDrawer::InvalidateGlyphs() + { + m_glyphUpdated = false; + } + /*! * \class Nz::RichTextDrawer::BlockRef * \brief Helper class representing a block inside a RichTextDrawer, allowing easier access. @@ -58,7 +286,7 @@ namespace Nz * * \see GetCharacterSize, GetColor, GetFont, GetText, SetStyle */ - inline UInt32 RichTextDrawer::BlockRef::GetStyle() const + inline TextStyleFlags RichTextDrawer::BlockRef::GetStyle() const { return m_drawer.GetBlockStyle(m_blockIndex); } @@ -113,7 +341,7 @@ namespace Nz * * \see GetStyle, SetCharacterSize, SetColor, SetFont, SetText */ - inline void RichTextDrawer::BlockRef::SetStyle(UInt32 style) + inline void RichTextDrawer::BlockRef::SetStyle(TextStyleFlags style) { m_drawer.SetBlockStyle(m_blockIndex, style); } diff --git a/src/Nazara/Utility/RichTextDrawer.cpp b/src/Nazara/Utility/RichTextDrawer.cpp new file mode 100644 index 000000000..5480a5c16 --- /dev/null +++ b/src/Nazara/Utility/RichTextDrawer.cpp @@ -0,0 +1,476 @@ +// Copyright (C) 2017 Jérôme Leclercq +// This file is part of the "Nazara Engine - Utility module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#include +#include +#include +#include + +namespace Nz +{ + RichTextDrawer::RichTextDrawer() : + m_defaultColor(Color::White), + //m_outlineColor(Color::Black), + m_defaultStyle(TextStyle_Regular), + m_glyphUpdated(true), + //m_maxLineWidth(std::numeric_limits::infinity()), + //m_outlineThickness(0.f), + m_defaultCharacterSize(24) + { + SetDefaultFont(Font::GetDefault()); + } + + RichTextDrawer::RichTextDrawer(const RichTextDrawer& drawer) : + m_defaultColor(drawer.m_defaultColor), + m_defaultStyle(drawer.m_defaultStyle), + m_fontIndexes(drawer.m_fontIndexes), + m_blocks(drawer.m_blocks), + m_glyphUpdated(false), + //m_outlineColor(drawer.m_outlineColor), + //m_maxLineWidth(drawer.m_maxLineWidth), + //m_outlineThickness(drawer.m_outlineThickness), + m_defaultCharacterSize(drawer.m_defaultCharacterSize) + { + m_fonts.resize(drawer.m_fonts.size()); + for (std::size_t i = 0; i < m_fonts.size(); ++i) + { + m_fonts[i].font = drawer.m_fonts[i].font; + m_fonts[i].useCount = drawer.m_fonts[i].useCount; + } + + SetDefaultFont(drawer.m_defaultFont); + + ConnectFontSlots(); + } + + RichTextDrawer::RichTextDrawer(RichTextDrawer&& drawer) + { + operator=(std::move(drawer)); + } + + RichTextDrawer::~RichTextDrawer() = default; + + auto RichTextDrawer::AppendText(const String& str) -> BlockRef + { + NazaraAssert(!str.IsEmpty(), "String cannot be empty"); + + std::size_t defaultFontIndex = HandleFontAddition(m_defaultFont); + + auto HasDefaultProperties = [&](const Block& block) + { + return block.characterSize == m_defaultCharacterSize && + block.color == m_defaultColor && + block.fontIndex == defaultFontIndex && + block.style == m_defaultStyle; + }; + + // Check if last block has the same property as default, else create a new block + if (m_blocks.empty() || !HasDefaultProperties(m_blocks.back())) + { + m_blocks.emplace_back(); + Block& newBlock = m_blocks.back(); + newBlock.characterSize = m_defaultCharacterSize; + newBlock.color = m_defaultColor; + newBlock.fontIndex = defaultFontIndex; + newBlock.style = m_defaultStyle; + newBlock.text = str; + + assert(newBlock.fontIndex < m_fonts.size()); + m_fonts[newBlock.fontIndex].useCount++; + } + else + m_blocks.back().text += str; + + InvalidateGlyphs(); + + return BlockRef(*this, m_blocks.size() - 1); + } + + const Recti& RichTextDrawer::GetBounds() const + { + if (!m_glyphUpdated) + UpdateGlyphs(); + + return m_bounds; + } + + Font* RichTextDrawer::GetFont(std::size_t index) const + { + NazaraAssert(index < m_fonts.size(), "Font index out of range"); + + return m_fonts[index].font; + } + + std::size_t RichTextDrawer::GetFontCount() const + { + return m_fonts.size(); + } + + const AbstractTextDrawer::Glyph& RichTextDrawer::GetGlyph(std::size_t index) const + { + if (!m_glyphUpdated) + UpdateGlyphs(); + + return m_glyphs[index]; + } + + std::size_t RichTextDrawer::GetGlyphCount() const + { + if (!m_glyphUpdated) + UpdateGlyphs(); + + return m_glyphs.size(); + } + + const AbstractTextDrawer::Line& RichTextDrawer::GetLine(std::size_t index) const + { + if (!m_glyphUpdated) + UpdateGlyphs(); + + NazaraAssert(index < m_lines.size(), "Line index out of range"); + return m_lines[index]; + } + + std::size_t RichTextDrawer::GetLineCount() const + { + if (!m_glyphUpdated) + UpdateGlyphs(); + + return m_lines.size(); + } + + void RichTextDrawer::MergeBlocks() + { + if (m_blocks.size() < 2) + return; + + auto TestBlockProperties = [](const Block& lhs, const Block& rhs) + { + return lhs.characterSize == rhs.characterSize && + lhs.color == rhs.color && + lhs.fontIndex == rhs.fontIndex && + lhs.style == rhs.style; + }; + + auto lastBlockIt = m_blocks.begin(); + for (auto it = lastBlockIt + 1; it != m_blocks.end();) + { + if (TestBlockProperties(*lastBlockIt, *it)) + { + // Append text to previous block and erase + lastBlockIt->text += it->text; + + ReleaseFont(it->fontIndex); + it = m_blocks.erase(it); + } + else + { + lastBlockIt = it; + ++it; + } + } + } + + void RichTextDrawer::RemoveBlock(std::size_t index) + { + NazaraAssert(index < m_blocks.size(), "Invalid block index"); + + ReleaseFont(m_blocks[index].fontIndex); + m_blocks.erase(m_blocks.begin() + index); + } + + RichTextDrawer& RichTextDrawer::operator=(const RichTextDrawer& drawer) + { + DisconnectFontSlots(); + + m_blocks = drawer.m_blocks; + m_defaultCharacterSize = drawer.m_defaultCharacterSize; + m_defaultColor = drawer.m_defaultColor; + m_defaultFont = drawer.m_defaultFont; + m_defaultStyle = drawer.m_defaultStyle; + m_fontIndexes = drawer.m_fontIndexes; + + m_fonts.resize(drawer.m_fonts.size()); + for (std::size_t i = 0; i < m_fonts.size(); ++i) + { + m_fonts[i].font = drawer.m_fonts[i].font; + m_fonts[i].useCount = drawer.m_fonts[i].useCount; + } + + ConnectFontSlots(); + InvalidateGlyphs(); + + return *this; + } + + RichTextDrawer& RichTextDrawer::operator=(RichTextDrawer&& drawer) + { + m_blocks = std::move(drawer.m_blocks); + m_bounds = std::move(m_bounds); + m_defaultCharacterSize = std::move(drawer.m_defaultCharacterSize); + m_defaultColor = std::move(drawer.m_defaultColor); + m_defaultFont = std::move(drawer.m_defaultFont); + m_defaultStyle = std::move(drawer.m_defaultStyle); + m_drawPos = std::move(m_drawPos); + m_fontIndexes = std::move(drawer.m_fontIndexes); + m_fonts = std::move(drawer.m_fonts); + m_glyphs = std::move(m_glyphs); + m_lines = std::move(m_lines); + m_glyphUpdated = std::move(m_glyphUpdated); + m_workingBounds = std::move(m_workingBounds); + + drawer.DisconnectFontSlots(); + ConnectFontSlots(); + + return *this; + } + + bool RichTextDrawer::GenerateGlyph(Glyph& glyph, char32_t character, float outlineThickness, bool lineWrap, const Font* font, const Color& color, TextStyleFlags style, unsigned int characterSize, int renderOrder, int* advance) const + { + const Font::Glyph& fontGlyph = font->GetGlyph(characterSize, style, outlineThickness, character); + if (fontGlyph.valid && fontGlyph.fauxOutlineThickness <= 0.f) + { + glyph.atlas = font->GetAtlas()->GetLayer(fontGlyph.layerIndex); + glyph.atlasRect = fontGlyph.atlasRect; + glyph.color = color; + glyph.flipped = fontGlyph.flipped; + glyph.renderOrder = renderOrder; + + glyph.bounds.Set(fontGlyph.aabb); + + //if (lineWrap && ShouldLineWrap(glyph, glyph.bounds.width)) + // AppendNewLine(m_lastSeparatorGlyph, m_lastSeparatorPosition); + + glyph.bounds.x += m_drawPos.x; + glyph.bounds.y += m_drawPos.y; + + // Faux bold and faux outline thickness are not supported + + // We "lean" the glyph to simulate italics style + float italic = (fontGlyph.requireFauxItalic) ? 0.208f : 0.f; + float italicTop = italic * glyph.bounds.y; + float italicBottom = italic * glyph.bounds.GetMaximum().y; + + glyph.corners[0].Set(glyph.bounds.x - italicTop - outlineThickness, glyph.bounds.y - outlineThickness); + glyph.corners[1].Set(glyph.bounds.x + glyph.bounds.width - italicTop - outlineThickness, glyph.bounds.y - outlineThickness); + glyph.corners[2].Set(glyph.bounds.x - italicBottom - outlineThickness, glyph.bounds.y + glyph.bounds.height - outlineThickness); + glyph.corners[3].Set(glyph.bounds.x + glyph.bounds.width - italicBottom - outlineThickness, glyph.bounds.y + glyph.bounds.height - outlineThickness); + + if (advance) + *advance = fontGlyph.advance; + + return true; + } + else + return false; + }; + + void RichTextDrawer::GenerateGlyphs(const Font* font, const Color& color, TextStyleFlags style, unsigned int characterSize, const Color& outlineColor, float outlineThickness, const String& text) const + { + if (text.IsEmpty()) + return; + + ///TODO: Allow iteration on Unicode characters without allocating any buffer + std::u32string characters = text.GetUtf32String(); + if (characters.empty()) + { + NazaraError("Invalid character set"); + return; + } + + char32_t previousCharacter = 0; + + const Font::SizeInfo& sizeInfo = font->GetSizeInfo(characterSize); + + float heightDifference = sizeInfo.lineHeight - m_lines.back().bounds.height; + if (heightDifference > 0.f) + { + for (std::size_t glyphIndex = m_lines.back().glyphIndex; glyphIndex < m_glyphs.size(); ++glyphIndex) + { + Glyph& glyph = m_glyphs[glyphIndex]; + glyph.bounds.y += heightDifference; + + for (auto& corner : glyph.corners) + corner.y += heightDifference; + } + + m_drawPos.y += heightDifference; + m_lines.back().bounds.height += heightDifference; + } + /*if (firstFont.font) + m_lines.emplace_back(Line{ Rectf(0.f, 0.f, 0.f, float(font->GetSizeInfo(firstBlock.characterSize).lineHeight)), 0 }); + else + m_lines.emplace_back(Line{ Rectf::Zero(), 0 });*/ + + m_glyphs.reserve(m_glyphs.size() + characters.size() * ((outlineThickness > 0.f) ? 2 : 1)); + for (char32_t character : characters) + { + if (previousCharacter != 0) + m_drawPos.x += font->GetKerning(characterSize, previousCharacter, character); + + previousCharacter = character; + + bool whitespace = true; + int advance = 0; + switch (character) + { + case ' ': + case '\n': + advance = sizeInfo.spaceAdvance; + break; + + case '\t': + advance = sizeInfo.spaceAdvance * 4; + break; + + default: + whitespace = false; + break; + } + + Glyph glyph; + if (!whitespace) + { + if (!GenerateGlyph(glyph, character, 0.f, true, font, color, style, characterSize, 0, &advance)) + continue; // Glyph failed to load, just skip it (can't do much) + + if (outlineThickness > 0.f) + { + Glyph outlineGlyph; + if (GenerateGlyph(outlineGlyph, character, outlineThickness, false, font, outlineColor, style, characterSize, -1, nullptr)) + { + m_glyphs.push_back(outlineGlyph); + } + } + } + else + { + float glyphAdvance = advance; + + //if (ShouldLineWrap(glyph, glyphAdvance)) + // AppendNewLine(m_lastSeparatorGlyph, m_lastSeparatorPosition); + + glyph.atlas = nullptr; + glyph.bounds.Set(float(m_drawPos.x), m_lines.back().bounds.y, glyphAdvance, float(sizeInfo.lineHeight)); + + glyph.corners[0].Set(glyph.bounds.GetCorner(RectCorner_LeftTop)); + glyph.corners[1].Set(glyph.bounds.GetCorner(RectCorner_RightTop)); + glyph.corners[2].Set(glyph.bounds.GetCorner(RectCorner_LeftBottom)); + glyph.corners[3].Set(glyph.bounds.GetCorner(RectCorner_RightBottom)); + } + + m_lines.back().bounds.ExtendTo(glyph.bounds); + + switch (character) + { + case '\n': + { + AppendNewLine(font, characterSize); + break; + } + + default: + m_drawPos.x += advance; + break; + } + + /*if (whitespace) + { + m_lastSeparatorGlyph = m_glyphs.size(); + m_lastSeparatorPosition = m_drawPos.x; + }*/ + + m_glyphs.push_back(glyph); + } + + m_workingBounds.ExtendTo(m_lines.back().bounds); + + m_bounds.Set(Rectf(std::floor(m_workingBounds.x), std::floor(m_workingBounds.y), std::ceil(m_workingBounds.width), std::ceil(m_workingBounds.height))); + + m_glyphUpdated = true; + } + + void RichTextDrawer::OnFontAtlasLayerChanged(const Font* font, AbstractImage* oldLayer, AbstractImage* newLayer) + { + NazaraUnused(font); + +#ifdef NAZARA_DEBUG + auto it = std::find_if(m_fonts.begin(), m_fonts.end(), [font](const auto& fontData) { return fontData.font == font; }); + if (it == m_fonts.end()) + { + NazaraInternalError("Not listening to " + String::Pointer(font)); + return; + } +#endif + + // Update atlas layer pointer + // Note: This can happen while updating + for (Glyph& glyph : m_glyphs) + { + if (glyph.atlas == oldLayer) + glyph.atlas = newLayer; + } + } + + void RichTextDrawer::OnFontInvalidated(const Font* font) + { + NazaraUnused(font); + +#ifdef NAZARA_DEBUG + auto it = std::find_if(m_fonts.begin(), m_fonts.end(), [font](const auto& fontData) { return fontData.font == font; }); + if (it == m_fonts.end()) + { + NazaraInternalError("Not listening to " + String::Pointer(font)); + return; + } +#endif + + m_glyphUpdated = false; + } + + void RichTextDrawer::OnFontRelease(const Font* font) + { + NazaraUnused(font); + NazaraUnused(font); + +#ifdef NAZARA_DEBUG + auto it = std::find_if(m_fonts.begin(), m_fonts.end(), [font](const auto& fontData) { return fontData.font == font; }); + if (it == m_fonts.end()) + { + NazaraInternalError("Not listening to " + String::Pointer(font)); + return; + } +#endif + + //SetFont(nullptr); + } + + void RichTextDrawer::UpdateGlyphs() const + { + ClearGlyphs(); + + if (!m_blocks.empty()) + { + const Block& firstBlock = m_blocks.front(); + + assert(firstBlock.fontIndex < m_fonts.size()); + const auto& firstFont = m_fonts[firstBlock.fontIndex]; + + if (firstFont.font) + m_lines.emplace_back(Line{ Rectf(0.f, 0.f, 0.f, float(firstFont.font->GetSizeInfo(firstBlock.characterSize).lineHeight)), 0 }); + else + m_lines.emplace_back(Line{ Rectf::Zero(), 0 }); + + m_drawPos.Set(0, firstBlock.characterSize); + + for (const Block& block : m_blocks) + { + assert(block.fontIndex < m_fonts.size()); + const auto& fontData = m_fonts[block.fontIndex]; + + GenerateGlyphs(fontData.font, block.color, block.style, block.characterSize, block.color, 0.f, block.text); + } + } + } +} From 8e4df4cadc79892a695ab0fe9802c3ef2519d882 Mon Sep 17 00:00:00 2001 From: Lynix Date: Sun, 22 Sep 2019 12:56:07 +0200 Subject: [PATCH 5/8] SDK: Add RichTextAreaWidget (WIP) --- ChangeLog.md | 2 + SDK/include/NDK/Console.hpp | 3 +- SDK/include/NDK/Widgets.hpp | 2 + .../NDK/Widgets/AbstractTextAreaWidget.hpp | 135 ++++ .../NDK/Widgets/AbstractTextAreaWidget.inl | 252 ++++++++ .../NDK/Widgets/RichTextAreaWidget.hpp | 58 ++ .../NDK/Widgets/RichTextAreaWidget.inl | 38 ++ SDK/include/NDK/Widgets/TextAreaWidget.hpp | 101 +-- SDK/include/NDK/Widgets/TextAreaWidget.inl | 243 -------- SDK/src/NDK/Console.cpp | 12 +- .../NDK/Widgets/AbstractTextAreaWidget.cpp | 489 +++++++++++++++ SDK/src/NDK/Widgets/RichTextAreaWidget.cpp | 156 +++++ SDK/src/NDK/Widgets/TextAreaWidget.cpp | 583 +++--------------- include/Nazara/Utility.hpp | 1 + include/Nazara/Utility/AbstractTextDrawer.hpp | 5 + include/Nazara/Utility/RichTextDrawer.hpp | 19 +- include/Nazara/Utility/RichTextDrawer.inl | 65 +- include/Nazara/Utility/SimpleTextDrawer.hpp | 2 +- src/Nazara/Utility/RichTextDrawer.cpp | 39 +- 19 files changed, 1346 insertions(+), 859 deletions(-) create mode 100644 SDK/include/NDK/Widgets/AbstractTextAreaWidget.hpp create mode 100644 SDK/include/NDK/Widgets/AbstractTextAreaWidget.inl create mode 100644 SDK/include/NDK/Widgets/RichTextAreaWidget.hpp create mode 100644 SDK/include/NDK/Widgets/RichTextAreaWidget.inl create mode 100644 SDK/src/NDK/Widgets/AbstractTextAreaWidget.cpp create mode 100644 SDK/src/NDK/Widgets/RichTextAreaWidget.cpp 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 } } From 1ea653ab5bf7f57764f680c429fe366935343799 Mon Sep 17 00:00:00 2001 From: Lynix Date: Sun, 22 Sep 2019 13:07:00 +0200 Subject: [PATCH 6/8] Console: Add support for text color --- ChangeLog.md | 1 + SDK/include/NDK/Console.hpp | 5 +++-- SDK/include/NDK/Console.inl | 2 +- SDK/src/NDK/Console.cpp | 5 ++--- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 07a90646e..a1f22e9e8 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -285,6 +285,7 @@ Nazara Development Kit: - ⚠️ Console class is no longer bound to a LuaState and now has a OnCommand signal - ⚠️ Made AbstractTextAreaWidget which is inherited by TextAreaWidget - ⚠️ Added RichTextAreaWidget +- ⚠️ Console now supports text color in history # 0.4: diff --git a/SDK/include/NDK/Console.hpp b/SDK/include/NDK/Console.hpp index 796a15588..3b4dbbe39 100644 --- a/SDK/include/NDK/Console.hpp +++ b/SDK/include/NDK/Console.hpp @@ -27,6 +27,7 @@ namespace Ndk class AbstractTextAreaWidget; class Console; class Entity; + class RichTextAreaWidget; class ScrollAreaWidget; class TextAreaWidget; @@ -46,7 +47,7 @@ namespace Ndk void ClearFocus(); inline unsigned int GetCharacterSize() const; - inline const TextAreaWidget* GetHistory() const; + inline const RichTextAreaWidget* GetHistory() const; inline const TextAreaWidget* GetInput() const; inline const Nz::FontRef& GetTextFont() const; @@ -73,7 +74,7 @@ namespace Ndk std::vector m_commandHistory; std::vector m_historyLines; ScrollAreaWidget* m_historyArea; - TextAreaWidget* m_history; + RichTextAreaWidget* m_history; TextAreaWidget* m_input; Nz::FontRef m_defaultFont; unsigned int m_characterSize; diff --git a/SDK/include/NDK/Console.inl b/SDK/include/NDK/Console.inl index 0e5733f75..3cd318ae3 100644 --- a/SDK/include/NDK/Console.inl +++ b/SDK/include/NDK/Console.inl @@ -19,7 +19,7 @@ namespace Ndk * \return History of the console */ - inline const TextAreaWidget* Console::GetHistory() const + inline const RichTextAreaWidget* Console::GetHistory() const { return m_history; } diff --git a/SDK/src/NDK/Console.cpp b/SDK/src/NDK/Console.cpp index 292ea992c..67f3e69b2 100644 --- a/SDK/src/NDK/Console.cpp +++ b/SDK/src/NDK/Console.cpp @@ -10,8 +10,6 @@ #include #include -///TODO: For now is unable to display different color in the history, it needs a RichTextDrawer to do so - namespace Ndk { namespace @@ -42,7 +40,7 @@ namespace Ndk m_maxHistoryLines(200) { // History - m_history = Add(); + m_history = Add(); m_history->EnableBackground(true); m_history->EnableLineWrap(true); m_history->SetReadOnly(true); @@ -112,6 +110,7 @@ namespace Ndk m_historyLines.erase(m_historyLines.begin()); m_historyLines.emplace_back(Line{ color, text }); + m_history->SetTextColor(color); m_history->AppendText(text + '\n'); m_history->Resize(m_history->GetPreferredSize()); m_historyArea->Resize(m_historyArea->GetSize()); From 3c7addc2629118e513028520ce48c513cecfce26 Mon Sep 17 00:00:00 2001 From: Lynix Date: Tue, 24 Sep 2019 18:44:12 +0200 Subject: [PATCH 7/8] Utility/RichTextDrawer: Fixes block removal not adjusting glyph indexes + Add HasBlocks method --- include/Nazara/Utility/RichTextDrawer.hpp | 4 ++- include/Nazara/Utility/RichTextDrawer.inl | 23 ++++++++++++++--- src/Nazara/Utility/RichTextDrawer.cpp | 31 ++++++++++++----------- 3 files changed, 38 insertions(+), 20 deletions(-) diff --git a/include/Nazara/Utility/RichTextDrawer.hpp b/include/Nazara/Utility/RichTextDrawer.hpp index c2d288109..218c207fe 100644 --- a/include/Nazara/Utility/RichTextDrawer.hpp +++ b/include/Nazara/Utility/RichTextDrawer.hpp @@ -54,6 +54,8 @@ namespace Nz std::size_t GetLineCount() const override; float GetMaxLineWidth() const override; + inline bool HasBlocks() const; + void MergeBlocks(); void RemoveBlock(std::size_t index); @@ -62,7 +64,7 @@ namespace Nz inline void SetBlockColor(std::size_t index, const Color& color); inline void SetBlockFont(std::size_t index, FontRef font); inline void SetBlockStyle(std::size_t index, TextStyleFlags style); - inline void SetBlockText(std::size_t index, const String& str); + inline void SetBlockText(std::size_t index, String str); inline void SetDefaultCharacterSize(unsigned int characterSize); inline void SetDefaultColor(const Color& color); diff --git a/include/Nazara/Utility/RichTextDrawer.inl b/include/Nazara/Utility/RichTextDrawer.inl index f258d0596..7faaf8b90 100644 --- a/include/Nazara/Utility/RichTextDrawer.inl +++ b/include/Nazara/Utility/RichTextDrawer.inl @@ -9,6 +9,16 @@ namespace Nz { inline std::size_t RichTextDrawer::FindBlock(std::size_t glyphIndex) const { + auto it = m_blocks.begin(); + for (; it != m_blocks.end(); ++it) + { + if (it->glyphIndex > glyphIndex) + break; + } + + assert(it != m_blocks.begin()); + return std::distance(m_blocks.begin(), it) - 1; + /* // Binary search std::size_t count = m_blocks.size(); std::size_t step; @@ -25,14 +35,14 @@ namespace Nz if (m_blocks[i].glyphIndex < glyphIndex) { - first = i + 1; + first = ++i; count -= step + 1; } else count = step; } - return i; + return i;*/ } inline auto RichTextDrawer::GetBlock(std::size_t index) -> BlockRef @@ -193,6 +203,11 @@ namespace Nz } } + inline bool RichTextDrawer::HasBlocks() const + { + return !m_blocks.empty(); + } + inline void RichTextDrawer::SetBlockCharacterSize(std::size_t index, unsigned int characterSize) { NazaraAssert(index < m_blocks.size(), "Invalid block index"); @@ -232,13 +247,13 @@ namespace Nz InvalidateGlyphs(); } - inline void RichTextDrawer::SetBlockText(std::size_t index, const String& str) + inline void RichTextDrawer::SetBlockText(std::size_t index, String str) { NazaraAssert(index < m_blocks.size(), "Invalid block index"); std::size_t previousLength = m_blocks[index].text.GetLength(); - m_blocks[index].text = str; + m_blocks[index].text = std::move(str); std::size_t newLength = m_blocks[index].text.GetLength(); if (newLength != previousLength) diff --git a/src/Nazara/Utility/RichTextDrawer.cpp b/src/Nazara/Utility/RichTextDrawer.cpp index 481fd35d1..142a4b395 100644 --- a/src/Nazara/Utility/RichTextDrawer.cpp +++ b/src/Nazara/Utility/RichTextDrawer.cpp @@ -166,9 +166,6 @@ namespace Nz void RichTextDrawer::MergeBlocks() { - if (m_blocks.size() < 2) - return; - auto TestBlockProperties = [](const Block& lhs, const Block& rhs) { return lhs.characterSize == rhs.characterSize && @@ -177,22 +174,16 @@ namespace Nz lhs.style == rhs.style; }; - auto lastBlockIt = m_blocks.begin(); - for (auto it = lastBlockIt + 1; it != m_blocks.end();) + std::size_t previousBlockIndex = 0; + for (std::size_t i = 1; i < m_blocks.size(); ++i) { - if (TestBlockProperties(*lastBlockIt, *it)) + if (TestBlockProperties(m_blocks[previousBlockIndex], m_blocks[i])) { - // Append text to previous block and erase - lastBlockIt->text += it->text; - - ReleaseFont(it->fontIndex); - it = m_blocks.erase(it); + RemoveBlock(i); + --i; } else - { - lastBlockIt = it; - ++it; - } + previousBlockIndex = i; } } @@ -200,8 +191,18 @@ namespace Nz { NazaraAssert(index < m_blocks.size(), "Invalid block index"); + std::size_t textLength = m_blocks[index].text.GetLength(); + ReleaseFont(m_blocks[index].fontIndex); m_blocks.erase(m_blocks.begin() + index); + + for (std::size_t i = index; i < m_blocks.size(); ++i) + { + assert(m_blocks[i].glyphIndex > textLength); + m_blocks[i].glyphIndex -= textLength; + } + + InvalidateGlyphs(); } void RichTextDrawer::SetMaxLineWidth(float lineWidth) From 0b5bc8a65639c4098fc612f4aa8e68716d757498 Mon Sep 17 00:00:00 2001 From: Lynix Date: Tue, 24 Sep 2019 18:50:45 +0200 Subject: [PATCH 8/8] SDK/RichTextAreaWidget: Fix erase/write behaviors --- SDK/src/NDK/Widgets/RichTextAreaWidget.cpp | 72 +++++++++++++++++----- 1 file changed, 56 insertions(+), 16 deletions(-) diff --git a/SDK/src/NDK/Widgets/RichTextAreaWidget.cpp b/SDK/src/NDK/Widgets/RichTextAreaWidget.cpp index a80c0e29d..9e5ffb84d 100644 --- a/SDK/src/NDK/Widgets/RichTextAreaWidget.cpp +++ b/SDK/src/NDK/Widgets/RichTextAreaWidget.cpp @@ -63,7 +63,7 @@ namespace Ndk return; std::size_t firstBlock = m_drawer.FindBlock(firstGlyph); - std::size_t lastBlock = m_drawer.FindBlock(lastGlyph); + std::size_t lastBlock = m_drawer.FindBlock((lastGlyph > 0) ? lastGlyph - 1 : lastGlyph); if (firstBlock == lastBlock) { const Nz::String& blockText = m_drawer.GetBlockText(firstBlock); @@ -79,18 +79,52 @@ namespace Ndk } if (lastGlyph < textLength) - { - std::size_t characterPosition = blockText.GetCharacterPosition(lastGlyph - blockFirstGlyph); - NazaraAssert(characterPosition != Nz::String::npos, "Invalid character position"); + newText.Append(blockText.SubString(blockText.GetCharacterPosition(lastGlyph - blockFirstGlyph))); - newText.Append(blockText.SubString(characterPosition)); - } - - m_drawer.SetBlockText(firstBlock, newText); + if (!newText.IsEmpty()) + m_drawer.SetBlockText(firstBlock, std::move(newText)); + else + m_drawer.RemoveBlock(firstBlock); } else { - // More complicated algorithm, yay + const Nz::String& lastBlockText = m_drawer.GetBlockText(lastBlock); + std::size_t lastBlockGlyphIndex = m_drawer.GetBlockFirstGlyphIndex(lastBlock); + + // First, update/delete last block + std::size_t lastCharPos = lastBlockText.GetCharacterPosition(lastGlyph - lastBlockGlyphIndex); + if (lastCharPos != Nz::String::npos) + { + Nz::String newText = lastBlockText.SubString(lastCharPos); + if (!newText.IsEmpty()) + 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 Nz::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 = firstBlockText.GetCharacterPosition(firstGlyph - firstBlockGlyphIndex - 1); + if (firstCharPos != Nz::String::npos) + { + Nz::String newText = firstBlockText.SubString(0, firstCharPos); + if (!newText.IsEmpty()) + m_drawer.SetBlockText(firstBlock, std::move(newText)); + else + m_drawer.RemoveBlock(firstBlock); + } + } + else + m_drawer.RemoveBlock(firstBlock); } UpdateDisplayText(); @@ -98,15 +132,21 @@ namespace Ndk 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); + 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); - Nz::String blockText = block.GetText(); - std::size_t characterPosition = blockText.GetCharacterPosition(glyphPosition - firstGlyph); - blockText.Insert(characterPosition, text); + Nz::String blockText = block.GetText(); + std::size_t characterPosition = blockText.GetCharacterPosition(glyphPosition - firstGlyph); + blockText.Insert(characterPosition, text); + + block.SetText(blockText); + } + else + m_drawer.AppendText(text); - block.SetText(blockText); SetCursorPosition(glyphPosition + text.GetLength()); UpdateDisplayText();