Repository: ihhub/fheroes2
Pull request #9729: Fix clearing diacritic signs in movable text used in status bar
Author: login: Districh-ru
Labels: name: bug
description: Something doesn't work
name: ui
description: UI/GUI related stuff
Created: 2025-04-10T08:55:55Z | Merged: 2025-04-24T06:12:33Z

fix #9728
Relates to #9730 

This PR adds an `area()` method to the `Text` class that calculates the area that the displayed text string will occupy.
The multi-line text version of this method is not implemented in this PR and may be implemented later when needed.

https://github.com/user-attachments/assets/3d202691-d165-4fba-9b14-7cc65f827dd8


Linked issue #9728: Bleeding diacritics (accent marks) aren't always cleared when drawing new text
### Preliminary checks

- [x] I've checked that there aren't [**other open issues**](https://github.com/ihhub/fheroes2/issues?q=is%3Aissue) on the same topic.
- [x] I've checked that this issue is reproducible on the [**latest snapshot build**](https://ihhub.github.io/fheroes2/INSTALL.html#snapshots-latest-builds).

### Platform

macOS

### Describe the bug

When viewing languages with diacritics, they aren't always removed when drawing new text. See the screenshot where in the "hero" screen, the diacritic over the `Š` in `Štěstí` (Luck) skill is not removed when hovering over the `Ankh` artifact.

## Reproduction
1. Load the provided save (campaign)
2. Set language to `Čeština`
4. View the hero screen for Myrini (from screenshot)
5. Hover over `Štěstí` (Luck) skill
6. Hover over `Ankh` artifact

> [!NOTE]
> It will also trigger if you hover over `Útok` (Attack) skill

### Hovering over `Štěstí`
<img width="1080" alt="Image" src="https://github.com/user-attachments/assets/d6f3c179-542d-43bb-9558-ade2aa924349" />

### Then hovering over `Ankh`
<img width="830" alt="Image" src="https://github.com/user-attachments/assets/c687b405-2b54-4ea2-b6d3-7f9a87079f95" />


### Save file

[Good_1_0061.savc.zip](https://github.com/user-attachments/files/19671741/Good_1_0061.savc.zip)

### Additional info

Built with the following command on `macos`

```shell
make clean && make -j$(sysctl -n hw.ncpu) FHEROES2_MACOS_APP_BUNDLE=ON
```
commit SHA 32e15c77882e0e592a426491e5b8c9ebf8511494

> [!NOTE]
> This issue is somewhat improved with my most recent [PR](https://github.com/ihhub/fheroes2/pull/9720) 

### Example

<img width="604" alt="Image" src="https://github.com/user-attachments/assets/4cdff3e0-54c3-473d-813d-f77681f58ddb" />

Diff:
diff --git a/src/fheroes2/castle/castle_well.cpp b/src/fheroes2/castle/castle_well.cpp
index e39ff4a18a0..d198f2e29f5 100644
--- a/src/fheroes2/castle/castle_well.cpp
+++ b/src/fheroes2/castle/castle_well.cpp
@@ -387,7 +387,7 @@ void Castle::_wellRedrawBackground( fheroes2::Image & background ) const
 
     const fheroes2::Rect textRoi{ 0, bottomBarOffsetY, backgroundWidth, bottomBar.height() - 1 };
     fheroes2::Text text( _( "Town Population Information and Statistics" ), fheroes2::FontType::normalWhite() );
-    text.drawInRoi( 315 - text.width() / 2, bottomBarOffsetY + 2, background, textRoi );
+    text.drawInRoi( 315 - text.width() / 2, bottomBarOffsetY + 3, background, textRoi );
 
     const fheroes2::FontType statsFontType = fheroes2::FontType::smallWhite();
 
diff --git a/src/fheroes2/gui/ui_text.cpp b/src/fheroes2/gui/ui_text.cpp
index 750c3132009..85f48e58254 100644
--- a/src/fheroes2/gui/ui_text.cpp
+++ b/src/fheroes2/gui/ui_text.cpp
@@ -191,6 +191,35 @@ namespace
         return width;
     }
 
+    fheroes2::Rect getTextLineArea( const uint8_t * data, const int32_t size, const fheroes2::FontCharHandler & charHandler )
+    {
+        assert( data != nullptr && size > 0 );
+
+        const uint8_t * dataEnd = data + size;
+
+        const fheroes2::Sprite & firstCharSprite = charHandler.getSprite( *data );
+        fheroes2::Rect area( firstCharSprite.x(), firstCharSprite.y(), firstCharSprite.width(), firstCharSprite.height() );
+        ++data;
+
+        for ( ; data != dataEnd; ++data ) {
+            const fheroes2::Sprite & sprite = charHandler.getSprite( *data );
+
+            if ( const int32_t spriteY = sprite.y(); spriteY < area.y ) {
+                // This character sprite is drawn higher than all previous - update `height` and `y`.
+                area.height += area.y - spriteY;
+                area.y = spriteY;
+                area.height = std::max( area.height, sprite.height() );
+            }
+            else {
+                area.height = std::max( area.height, spriteY - area.y + sprite.height() );
+            }
+
+            area.width += sprite.x() + sprite.width();
+        }
+
+        return area;
+    }
+
     int32_t getMaxCharacterCount( const uint8_t * data, const int32_t size, const fheroes2::FontCharHandler & charHandler, const int32_t maxWidth )
     {
         assert( data != nullptr && size > 0 && maxWidth > 0 );
@@ -337,7 +366,7 @@ namespace fheroes2
     // TODO: Properly handle strings with many text lines ('\n'). Now their widths are counted as if they're one line.
     int32_t Text::width() const
     {
-        const auto langugeSwitcher = getLanguageSwitcher( *this );
+        const auto languageSwitcher = getLanguageSwitcher( *this );
         const fheroes2::FontCharHandler charHandler( _fontType );
 
         return getLineWidth( reinterpret_cast<const uint8_t *>( _text.data() ), static_cast<int32_t>( _text.size() ), charHandler, _keepLineTrailingSpaces );
@@ -346,7 +375,7 @@ namespace fheroes2
     // TODO: Properly handle strings with many text lines ('\n'). Now their heights are counted as if they're one line.
     int32_t Text::height() const
     {
-        const auto langugeSwitcher = getLanguageSwitcher( *this );
+        const auto languageSwitcher = getLanguageSwitcher( *this );
         return getFontHeight( _fontType.size );
     }
 
@@ -356,7 +385,7 @@ namespace fheroes2
             return 0;
         }
 
-        const auto langugeSwitcher = getLanguageSwitcher( *this );
+        const auto languageSwitcher = getLanguageSwitcher( *this );
         const int32_t fontHeight = height();
 
         std::vector<TextLineInfo> lineInfos;
@@ -399,7 +428,7 @@ namespace fheroes2
             return 0;
         }
 
-        const auto langugeSwitcher = getLanguageSwitcher( *this );
+        const auto languageSwitcher = getLanguageSwitcher( *this );
         const int32_t fontHeight = height();
 
         std::vector<TextLineInfo> lineInfos;
@@ -414,13 +443,25 @@ namespace fheroes2
             return 0;
         }
 
-        const auto langugeSwitcher = getLanguageSwitcher( *this );
+        const auto languageSwitcher = getLanguageSwitcher( *this );
         std::vector<TextLineInfo> lineInfos;
         getTextLineInfos( lineInfos, maxWidth, height(), false );
 
         return static_cast<int32_t>( lineInfos.size() );
     }
 
+    Rect Text::area() const
+    {
+        if ( _text.empty() ) {
+            return {};
+        }
+
+        const auto languageSwitcher = getLanguageSwitcher( *this );
+        const fheroes2::FontCharHandler charHandler( _fontType );
+
+        return getTextLineArea( reinterpret_cast<const uint8_t *>( _text.data() ), static_cast<int32_t>( _text.size() ), charHandler );
+    }
+
     void Text::drawInRoi( const int32_t x, const int32_t y, Image & output, const Rect & imageRoi ) const
     {
         if ( output.empty() || _text.empty() ) {
@@ -428,7 +469,7 @@ namespace fheroes2
             return;
         }
 
-        const auto langugeSwitcher = getLanguageSwitcher( *this );
+        const auto languageSwitcher = getLanguageSwitcher( *this );
         const fheroes2::FontCharHandler charHandler( _fontType );
 
         renderSingleLine( reinterpret_cast<const uint8_t *>( _text.data() ), static_cast<int32_t>( _text.size() ), x, y, output, imageRoi, charHandler );
@@ -446,7 +487,7 @@ namespace fheroes2
             return;
         }
 
-        const auto langugeSwitcher = getLanguageSwitcher( *this );
+        const auto languageSwitcher = getLanguageSwitcher( *this );
 
         std::vector<TextLineInfo> lineInfos;
         getTextLineInfos( lineInfos, maxWidth, height(), false );
@@ -479,7 +520,7 @@ namespace fheroes2
             return;
         }
 
-        const auto langugeSwitcher = getLanguageSwitcher( *this );
+        const auto languageSwitcher = getLanguageSwitcher( *this );
         const fheroes2::FontCharHandler charHandler( _fontType );
 
         const int32_t originalTextWidth
@@ -503,7 +544,7 @@ namespace fheroes2
             return;
         }
 
-        const auto langugeSwitcher = getLanguageSwitcher( *this );
+        const auto languageSwitcher = getLanguageSwitcher( *this );
         const fheroes2::FontCharHandler charHandler( _fontType );
         const int32_t fullLineWidth = getLineWidth( reinterpret_cast<const uint8_t *>( _text.data() ), static_cast<int32_t>( _text.size() ), charHandler, true );
         if ( fullLineWidth < maxWidth ) {
@@ -791,6 +832,36 @@ namespace fheroes2
         return static_cast<int32_t>( lineInfos.size() );
     }
 
+    Rect MultiFontText::area() const
+    {
+        Rect area;
+        bool isFirstText = true;
+
+        for ( const Text & text : _texts ) {
+            if ( isFirstText ) {
+                isFirstText = false;
+                area = text.area();
+                continue;
+            }
+
+            const fheroes2::Rect & textArea = text.area();
+
+            if ( textArea.y < area.y ) {
+                // This character sprite is drawn higher than all previous - update `height` and `y`.
+                area.height += area.y - textArea.y;
+                area.y = textArea.y;
+                area.height = std::max( area.height, textArea.height );
+            }
+            else {
+                area.height = std::max( area.height, textArea.y - area.y + textArea.height );
+            }
+
+            area.width += textArea.x + textArea.width;
+        }
+
+        return area;
+    }
+
     void MultiFontText::drawInRoi( const int32_t x, const int32_t y, Image & output, const Rect & imageRoi ) const
     {
         if ( output.empty() || _texts.empty() ) {
@@ -802,7 +873,7 @@ namespace fheroes2
 
         int32_t offsetX = x;
         for ( const Text & text : _texts ) {
-            const auto langugeSwitcher = getLanguageSwitcher( text );
+            const auto languageSwitcher = getLanguageSwitcher( text );
             const int32_t fontHeight = getFontHeight( text._fontType.size );
             const fheroes2::FontCharHandler charHandler( text._fontType );
 
@@ -847,7 +918,7 @@ namespace fheroes2
         auto infoIter = lineInfos.cbegin();
 
         for ( const Text & singleText : _texts ) {
-            const auto langugeSwitcher = getLanguageSwitcher( singleText );
+            const auto languageSwitcher = getLanguageSwitcher( singleText );
             const uint8_t * data = reinterpret_cast<const uint8_t *>( singleText._text.data() );
 
             const uint8_t * dataEnd = data + singleText._text.size();
@@ -886,7 +957,7 @@ namespace fheroes2
     {
         const size_t textsCount = _texts.size();
         for ( size_t i = 0; i < textsCount; ++i ) {
-            const auto langugeSwitcher = getLanguageSwitcher( _texts[i] );
+            const auto languageSwitcher = getLanguageSwitcher( _texts[i] );
 
             // To properly render a multi-font text we must not ignore spaces at the end of a text entry which is not the last one.
             const bool isNotLastTextEntry = ( i != textsCount - 1 );
diff --git a/src/fheroes2/gui/ui_text.h b/src/fheroes2/gui/ui_text.h
index 342e18a6042..68ee4070ee0 100644
--- a/src/fheroes2/gui/ui_text.h
+++ b/src/fheroes2/gui/ui_text.h
@@ -141,6 +141,9 @@ namespace fheroes2
         // Returns number of multi-line text rows limited by width of a line. It can be 0 if the text is empty.
         virtual int32_t rows( const int32_t maxWidth ) const = 0;
 
+        // Returns the text line ROI relative to the text line begin. It analyzes offset and size of all characters in the text.
+        virtual Rect area() const = 0;
+
         // Draw text as a single line text.
         void draw( const int32_t x, const int32_t y, Image & output ) const
         {
@@ -217,6 +220,8 @@ namespace fheroes2
 
         int32_t rows( const int32_t maxWidth ) const override;
 
+        Rect area() const override;
+
         void drawInRoi( const int32_t x, const int32_t y, Image & output, const Rect & imageRoi ) const override;
         void drawInRoi( const int32_t x, const int32_t y, const int32_t maxWidth, Image & output, const Rect & imageRoi ) const override;
 
@@ -309,6 +314,8 @@ namespace fheroes2
 
         int32_t rows( const int32_t maxWidth ) const override;
 
+        Rect area() const override;
+
         void drawInRoi( const int32_t x, const int32_t y, Image & output, const Rect & imageRoi ) const override;
         void drawInRoi( const int32_t x, const int32_t y, const int32_t maxWidth, Image & output, const Rect & imageRoi ) const override;
 
diff --git a/src/fheroes2/gui/ui_tool.cpp b/src/fheroes2/gui/ui_tool.cpp
index 4e822e189c4..13d0e823d2e 100644
--- a/src/fheroes2/gui/ui_tool.cpp
+++ b/src/fheroes2/gui/ui_tool.cpp
@@ -30,7 +30,6 @@
 #include <stdexcept>
 #include <string>
 #include <type_traits>
-#include <utility>
 
 #include "agg_image.h"
 #include "cursor.h"
@@ -182,45 +181,26 @@ namespace fheroes2
         }
     }
 
-    MovableText::MovableText( Image & output )
-        : _output( output )
-        , _restorer( output, 0, 0, 0, 0 )
-        , _isHidden( false )
-    {
-        // Do nothing.
-    }
-
-    void MovableText::update( std::unique_ptr<TextBase> text )
-    {
-        _text = std::move( text );
-    }
-
-    void MovableText::draw( const int32_t x, const int32_t y )
+    void MovableText::drawInRoi( const int32_t x, const int32_t y, const Rect & roi )
     {
         hide();
 
-        _restorer.update( x, y, _text->width(), _text->height() );
-        _text->draw( x, y, _output );
+        assert( _text != nullptr );
 
-        _isHidden = false;
-    }
+        Rect textArea = _text->area();
+        textArea.x += x;
+        textArea.y += y;
 
-    void MovableText::drawInRoi( const int32_t x, const int32_t y, const Rect & roi )
-    {
-        hide();
+        // Not to cut off the top of diacritic signs in capital letters we shift the text down.
+        const int32_t extraShiftY = textArea.y < roi.y ? roi.y - textArea.y : 0;
+        textArea.height += extraShiftY;
 
-        _restorer.update( x, y, _text->width(), _text->height() );
-        _text->drawInRoi( x, y, _output, roi );
+        const Rect overlappedRoi = textArea ^ roi;
 
-        _isHidden = false;
-    }
+        _restorer.update( overlappedRoi.x, overlappedRoi.y, overlappedRoi.width, overlappedRoi.height );
+        _text->drawInRoi( x, y + extraShiftY, _output, overlappedRoi );
 
-    void MovableText::hide()
-    {
-        if ( !_isHidden ) {
-            _restorer.restore();
-            _isHidden = true;
-        }
+        _isHidden = false;
     }
 
     SystemInfoRenderer::SystemInfoRenderer()
diff --git a/src/fheroes2/gui/ui_tool.h b/src/fheroes2/gui/ui_tool.h
index 038afeb27a8..6ee2c96f471 100644
--- a/src/fheroes2/gui/ui_tool.h
+++ b/src/fheroes2/gui/ui_tool.h
@@ -29,6 +29,7 @@
 #include <optional>
 #include <string>
 #include <string_view>
+#include <utility>
 #include <vector>
 
 #include "image.h"
@@ -92,7 +93,12 @@ namespace fheroes2
     class MovableText
     {
     public:
-        explicit MovableText( Image & output );
+        explicit MovableText( Image & output )
+            : _output( output )
+            , _restorer( output, 0, 0, 0, 0 )
+        {
+            // Do nothing.
+        }
 
         MovableText( const MovableText & ) = delete;
 
@@ -100,20 +106,32 @@ namespace fheroes2
 
         MovableText & operator=( const MovableText & ) = delete;
 
-        void update( std::unique_ptr<TextBase> text );
+        void update( std::unique_ptr<TextBase> text )
+        {
+            _text = std::move( text );
+        }
 
-        void draw( const int32_t x, const int32_t y );
+        void draw( const int32_t x, const int32_t y )
+        {
+            drawInRoi( x, y, { 0, 0, _output.width(), _output.height() } );
+        }
 
         // Draw text within a specified ROI (Region of Interest) that acts as a bounding box
         void drawInRoi( const int32_t x, const int32_t y, const Rect & roi );
 
-        void hide();
+        void hide()
+        {
+            if ( !_isHidden ) {
+                _restorer.restore();
+                _isHidden = true;
+            }
+        }
 
     private:
         Image & _output;
         ImageRestorer _restorer;
         std::unique_ptr<TextBase> _text;
-        bool _isHidden;
+        bool _isHidden{ true };
     };
 
     // Renderer of current time and FPS on screen


Reviews:
user: login: justinbburris
body: These changes look great and run great! I have only 1 nit (a typo) and the rest are just questions/enhancements we could consider making.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T02:14:20Z
user: login: Districh-ru
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-11T05:28:08Z
user: login: Districh-ru
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-11T05:30:26Z
user: login: Districh-ru
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-11T05:52:48Z
user: login: Districh-ru
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-11T05:59:58Z
user: login: Districh-ru
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-11T06:01:25Z
user: login: Districh-ru
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-11T06:11:01Z
user: login: Districh-ru
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-11T06:32:05Z
user: login: justinbburris
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T10:57:11Z
user: login: oleg-derevenetz
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-11T11:28:04Z
user: login: oleg-derevenetz
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-11T11:32:16Z
user: login: justinbburris
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T23:19:23Z
user: login: ihhub
state: APPROVED
author_association: OWNER
submitted_at: 2025-04-24T06:12:21Z

Review comments:
diff_hunk: @@ -191,6 +191,35 @@ namespace
         return width;
     }
 
+    fheroes2::Rect getTextLineArea( const uint8_t * data, const int32_t size, const fheroes2::FontCharHandler & charHandler )
+    {
+        assert( data != nullptr && size > 0 );
path: src/fheroes2/gui/ui_text.cpp
user: login: justinbburris
body: Question: Do we disable assertions in release builds? Is this a case that could happen in a release build? (Say if a translation was missing)
created_at: 2025-04-10T23:26:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -191,6 +191,35 @@ namespace
         return width;
     }
 
+    fheroes2::Rect getTextLineArea( const uint8_t * data, const int32_t size, const fheroes2::FontCharHandler & charHandler )
path: src/fheroes2/gui/ui_text.cpp
user: login: justinbburris
body: Question: Would it make sense to add the `[[nodiscard]]` attribute?
created_at: 2025-04-10T23:36:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -421,6 +450,14 @@ namespace fheroes2
         return static_cast<int32_t>( lineInfos.size() );
     }
 
+    Rect Text::area() const
path: src/fheroes2/gui/ui_text.cpp
user: login: justinbburris
body: Question: Same `[[nodiscard]]` question
created_at: 2025-04-10T23:37:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -421,6 +450,14 @@ namespace fheroes2
         return static_cast<int32_t>( lineInfos.size() );
     }
 
+    Rect Text::area() const
+    {
+        const auto langugeSwitcher = getLanguageSwitcher( *this );
path: src/fheroes2/gui/ui_text.cpp
user: login: justinbburris
body: nit: typo here 
```suggestion
        const auto languageSwitcher = getLanguageSwitcher( *this );
```
created_at: 2025-04-11T01:13:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -421,6 +450,14 @@ namespace fheroes2
         return static_cast<int32_t>( lineInfos.size() );
     }
 
+    Rect Text::area() const
+    {
+        const auto langugeSwitcher = getLanguageSwitcher( *this );
+        const fheroes2::FontCharHandler charHandler( _fontType );
+
+        return getTextLineArea( reinterpret_cast<const uint8_t *>( _text.data() ), static_cast<int32_t>( _text.size() ), charHandler );
path: src/fheroes2/gui/ui_text.cpp
user: login: justinbburris
body: Enhancement Opportunity (non blocking): should we return an empty rectangle if the text is empty?
created_at: 2025-04-11T01:16:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -421,6 +450,14 @@ namespace fheroes2
         return static_cast<int32_t>( lineInfos.size() );
     }
 
+    Rect Text::area() const
+    {
+        const auto langugeSwitcher = getLanguageSwitcher( *this );
+        const fheroes2::FontCharHandler charHandler( _fontType );
+
+        return getTextLineArea( reinterpret_cast<const uint8_t *>( _text.data() ), static_cast<int32_t>( _text.size() ), charHandler );
path: src/fheroes2/gui/ui_text.cpp
user: login: justinbburris
body: Question: also why do we do this casting here? Could we pass the text to the `getTextLineArea` method and have it do any kind of casting it needs to?
created_at: 2025-04-11T01:17:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -421,6 +450,14 @@ namespace fheroes2
         return static_cast<int32_t>( lineInfos.size() );
     }
 
+    Rect Text::area() const
path: src/fheroes2/gui/ui_text.cpp
user: login: justinbburris
body: Enhancement Opportunity (non blocking): Would it be nice to add some doxygen style comments explaining this method?

```
/// @brief Created a bounding rectangle for the text
/// @return Rect object containing the text's dimensions and position
/// @note This function assumes the text is properly initialized
```
created_at: 2025-04-11T01:21:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -182,47 +181,21 @@ namespace fheroes2
         }
     }
 
-    MovableText::MovableText( Image & output )
-        : _output( output )
-        , _restorer( output, 0, 0, 0, 0 )
-        , _isHidden( false )
-    {
-        // Do nothing.
-    }
-
-    void MovableText::update( std::unique_ptr<TextBase> text )
-    {
-        _text = std::move( text );
-    }
-
-    void MovableText::draw( const int32_t x, const int32_t y )
+    void MovableText::drawInRoi( const int32_t x, const int32_t y, const Rect & roi )
path: src/fheroes2/gui/ui_tool.cpp
user: login: justinbburris
body: Enhancement Opportunity (non blocking): I realize this isn't your code so please disregard if it's out of scope but would it make sense to use the `Point` type as the argument?

```c++
   struct Point {
       int32_t x;
       int32_t y;
   };
```
created_at: 2025-04-11T02:01:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -421,6 +450,14 @@ namespace fheroes2
         return static_cast<int32_t>( lineInfos.size() );
     }
 
+    Rect Text::area() const
path: src/fheroes2/gui/ui_text.cpp
user: login: Districh-ru
body: > Enhancement Opportunity (non blocking): Would it be nice to add some doxygen style comments explaining this method?

@justinbburris, please read: https://github.com/ihhub/fheroes2/pull/9672#discussion_r2009965776

created_at: 2025-04-11T05:28:08Z
author_association: COLLABORATOR
diff_hunk: @@ -421,6 +450,14 @@ namespace fheroes2
         return static_cast<int32_t>( lineInfos.size() );
     }
 
+    Rect Text::area() const
+    {
+        const auto langugeSwitcher = getLanguageSwitcher( *this );
path: src/fheroes2/gui/ui_text.cpp
user: login: Districh-ru
body: Fixed, thanks!
created_at: 2025-04-11T05:30:26Z
author_association: COLLABORATOR
diff_hunk: @@ -191,6 +191,35 @@ namespace
         return width;
     }
 
+    fheroes2::Rect getTextLineArea( const uint8_t * data, const int32_t size, const fheroes2::FontCharHandler & charHandler )
+    {
+        assert( data != nullptr && size > 0 );
path: src/fheroes2/gui/ui_text.cpp
user: login: Districh-ru
body: Assertions are enabled in release builds and this case should never happen in the `namespace` functions. We should handle the empty text case in methods that call this function.
created_at: 2025-04-11T05:52:48Z
author_association: COLLABORATOR
diff_hunk: @@ -191,6 +191,35 @@ namespace
         return width;
     }
 
+    fheroes2::Rect getTextLineArea( const uint8_t * data, const int32_t size, const fheroes2::FontCharHandler & charHandler )
path: src/fheroes2/gui/ui_text.cpp
user: login: Districh-ru
body: Currently we don't use this attribute in the code ...
Hi @oleg-derevenetz, I would appreciate your opinion on this matter.
created_at: 2025-04-11T05:59:58Z
author_association: COLLABORATOR
diff_hunk: @@ -421,6 +450,14 @@ namespace fheroes2
         return static_cast<int32_t>( lineInfos.size() );
     }
 
+    Rect Text::area() const
+    {
+        const auto langugeSwitcher = getLanguageSwitcher( *this );
+        const fheroes2::FontCharHandler charHandler( _fontType );
+
+        return getTextLineArea( reinterpret_cast<const uint8_t *>( _text.data() ), static_cast<int32_t>( _text.size() ), charHandler );
path: src/fheroes2/gui/ui_text.cpp
user: login: Districh-ru
body: Nice catch!
I've updated the code to return empty `Rect` if `_text.empty() == true`
created_at: 2025-04-11T06:01:25Z
author_association: COLLABORATOR
diff_hunk: @@ -421,6 +450,14 @@ namespace fheroes2
         return static_cast<int32_t>( lineInfos.size() );
     }
 
+    Rect Text::area() const
+    {
+        const auto langugeSwitcher = getLanguageSwitcher( *this );
+        const fheroes2::FontCharHandler charHandler( _fontType );
+
+        return getTextLineArea( reinterpret_cast<const uint8_t *>( _text.data() ), static_cast<int32_t>( _text.size() ), charHandler );
path: src/fheroes2/gui/ui_text.cpp
user: login: Districh-ru
body: `std::string` contains of `char`s (`int8_t`) while operating with fonts we use `uint8_t` (`unsigned char`) to more properly associate character code with the ASCII code (https://www.ascii-code.com/). So we do casting.

Not all the times we need to analyze the whole text sometimes it is needed to analyze only a part oа text so we can pass smaller `size` or move `data` pointer without making changes to the original `std::string`.

I'm preparing a PR for `textInput` that will use this to calculate data only for the shown part of the text in the text input.

created_at: 2025-04-11T06:11:01Z
author_association: COLLABORATOR
diff_hunk: @@ -182,47 +181,21 @@ namespace fheroes2
         }
     }
 
-    MovableText::MovableText( Image & output )
-        : _output( output )
-        , _restorer( output, 0, 0, 0, 0 )
-        , _isHidden( false )
-    {
-        // Do nothing.
-    }
-
-    void MovableText::update( std::unique_ptr<TextBase> text )
-    {
-        _text = std::move( text );
-    }
-
-    void MovableText::draw( const int32_t x, const int32_t y )
+    void MovableText::drawInRoi( const int32_t x, const int32_t y, const Rect & roi )
path: src/fheroes2/gui/ui_tool.cpp
user: login: Districh-ru
body: IMHO (I'm not a pro in C++),
since mostly the caller functions use separated x and y arguments and the called `_text->drawInRoi()` also uses `x` and `y` it might be redundant making a Point object when passing data to this method and then use the data separately. And I prefer to use the same argument types as in the similar methods. :)
On modern systems a reference to `Point` will be 8 bytes (64 bits) and passing two 32 bit integers will also occupy the same 8 bytes. So I choose to pass `x` and `y` separately despite I create a `Point` object inside the `drawInRoi()` method.


created_at: 2025-04-11T06:32:05Z
author_association: COLLABORATOR
diff_hunk: @@ -421,6 +450,14 @@ namespace fheroes2
         return static_cast<int32_t>( lineInfos.size() );
     }
 
+    Rect Text::area() const
path: src/fheroes2/gui/ui_text.cpp
user: login: justinbburris
body: Interesting and good to know, thanks!
created_at: 2025-04-11T10:57:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -421,6 +450,14 @@ namespace fheroes2
         return static_cast<int32_t>( lineInfos.size() );
     }
 
+    Rect Text::area() const
path: src/fheroes2/gui/ui_text.cpp
user: login: justinbburris
body: Interesting and good to know, thanks!
created_at: 2025-04-11T10:57:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -191,6 +191,35 @@ namespace
         return width;
     }
 
+    fheroes2::Rect getTextLineArea( const uint8_t * data, const int32_t size, const fheroes2::FontCharHandler & charHandler )
+    {
+        assert( data != nullptr && size > 0 );
path: src/fheroes2/gui/ui_text.cpp
user: login: oleg-derevenetz
body: @justinbburris

> Question: Do we disable assertions in release builds? Is this a case that could happen in a release build? (Say if a translation was missing)

In general, it's up to package maintainers whether they will build fheroes2 with or without assertions (using the `NDEBUG` macro). On our CI we build fheroes2 without `NDEBUG` (i.e. with assertions enabled) - except maybe the Android release build. In either case, if the preconditions are not met, the code will crash sooner or later, but with assertions it will crash sooner and will show us why. That's what we want: invalid code (I mean, invalid due to the programmer's error) should eventually crash and not pretend that "everything is OK". 
created_at: 2025-04-11T11:28:04Z
author_association: COLLABORATOR
diff_hunk: @@ -191,6 +191,35 @@ namespace
         return width;
     }
 
+    fheroes2::Rect getTextLineArea( const uint8_t * data, const int32_t size, const fheroes2::FontCharHandler & charHandler )
path: src/fheroes2/gui/ui_text.cpp
user: login: oleg-derevenetz
body: Hi @Districh-ru it's up to you. If there is a risk that the "user" of this method will most likely tend to ignore the result, then the use of this attribute can be justified. 
created_at: 2025-04-11T11:32:16Z
author_association: COLLABORATOR
diff_hunk: @@ -191,6 +191,35 @@ namespace
         return width;
     }
 
+    fheroes2::Rect getTextLineArea( const uint8_t * data, const int32_t size, const fheroes2::FontCharHandler & charHandler )
+    {
+        assert( data != nullptr && size > 0 );
path: src/fheroes2/gui/ui_text.cpp
user: login: justinbburris
body: thanks for the breakdown @oleg-derevenetz !
created_at: 2025-04-11T23:03:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -421,6 +450,14 @@ namespace fheroes2
         return static_cast<int32_t>( lineInfos.size() );
     }
 
+    Rect Text::area() const
+    {
+        const auto langugeSwitcher = getLanguageSwitcher( *this );
+        const fheroes2::FontCharHandler charHandler( _fontType );
+
+        return getTextLineArea( reinterpret_cast<const uint8_t *>( _text.data() ), static_cast<int32_t>( _text.size() ), charHandler );
path: src/fheroes2/gui/ui_text.cpp
user: login: justinbburris
body: ahhhh I get it. Thank you for the breakdown! I'm not used to having to think about the signs for the characters 😅 
created_at: 2025-04-11T23:15:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -182,47 +181,21 @@ namespace fheroes2
         }
     }
 
-    MovableText::MovableText( Image & output )
-        : _output( output )
-        , _restorer( output, 0, 0, 0, 0 )
-        , _isHidden( false )
-    {
-        // Do nothing.
-    }
-
-    void MovableText::update( std::unique_ptr<TextBase> text )
-    {
-        _text = std::move( text );
-    }
-
-    void MovableText::draw( const int32_t x, const int32_t y )
+    void MovableText::drawInRoi( const int32_t x, const int32_t y, const Rect & roi )
path: src/fheroes2/gui/ui_tool.cpp
user: login: justinbburris
body: Yeah - it would make sense to refactor the usage to either use `Point` or to use `x`, `y` across a bunch of methods which... is definitely out of scope. Thank you for the breakdown! 🙇 
created_at: 2025-04-11T23:19:01Z
author_association: CONTRIBUTOR

PR comments:
user: login: Districh-ru
created_at: 2025-04-10T09:17:57Z
body: Hi @justinbburris, could you please check this PR when you have time.
author_association: COLLABORATOR
user: login: justinbburris
created_at: 2025-04-10T23:02:10Z
body: One thing to consider with this work is that we've now clipped the bleeding diacritics. Is this intentional?

This [discussion](https://github.com/ihhub/fheroes2/pull/9720#issuecomment-2783896648) captures some of the options for how we might want to treat these diacritics.

Specifically these options
- Clip the diacritics **👈 It appears that this PR has picked this solution** 
- Shrink the text (😬)
- Clip the descenders
- Expand the size of the text box
- Do nothing

While I don't know that this is a problem for this PR - it does have implications for the pronunciation of those words - I'll leave the implications up for someone else to discuss (who is more fluent in those languages)

### Examples of clipping from this change
<img width="278" alt="Screenshot 2025-04-10 at 6 55 05 PM" src="https://github.com/user-attachments/assets/5d35daa7-856e-4b62-af73-73056d71c4f6" />
<img width="256" alt="Screenshot 2025-04-10 at 6 54 42 PM" src="https://github.com/user-attachments/assets/1ae3b631-1a11-40d1-afaf-d15b1f8614ee" />
author_association: CONTRIBUTOR
user: login: justinbburris
created_at: 2025-04-10T23:12:54Z
body: To ensure we're not blocking this PR on that discussion/issue resolution, I've created this issue from that comment
https://github.com/ihhub/fheroes2/issues/9730
author_association: CONTRIBUTOR
user: login: justinbburris
created_at: 2025-04-11T02:13:03Z
body: (I believe this is unrelated to your changes, but while we're here... 😅)

One thing I noticed while reviewing this PR is that the well status text is 1px higher. See this comparison screenshot.

<img width="1865" alt="comparison" src="https://github.com/user-attachments/assets/a75c9ce4-e99f-4810-8f0b-283f770eeb10" />

### Any other status bar
<img width="662" alt="other status bar" src="https://github.com/user-attachments/assets/61144e5f-ad2f-4630-98d8-6ccf488117ff" />

### Well status bar
<img width="1509" alt="well" src="https://github.com/user-attachments/assets/dc7afba7-c6d7-4e6f-bea2-8c1f3ae659a6" />

author_association: CONTRIBUTOR
user: login: Districh-ru
created_at: 2025-04-11T05:40:45Z
body: > One thing to consider with this work is that we've now clipped the bleeding diacritics. Is this intentional?

@justinbburris, this PR did not change this behavior, see the current master build. :)
And the diacritics cutting issue should be fixed but only after we discuss how to do this.

As a temporary solution in this PR - the text with diacritics in capital letters will be shifted down not to cut diacritics:
![изображение](https://github.com/user-attachments/assets/1a2013ee-4fc8-4710-a70e-95e253c33181)




author_association: COLLABORATOR
user: login: justinbburris
created_at: 2025-04-11T23:02:58Z
body: > > One thing to consider with this work is that we've now clipped the bleeding diacritics. Is this intentional?
> 
> @justinbburris, this PR did not change this behavior, see the current master build. :) And the diacritics cutting issue should be fixed but only after we discuss ow to do this.
> 
> As a temporary solution in this PR - the text with diacritics in capital letters will be shifted down not to cut diacritics: ![изображение](https://private-user-images.githubusercontent.com/113276641/432617016-1a2013ee-4fc8-4710-a70e-95e253c33181.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDQ0MTI3OTIsIm5iZiI6MTc0NDQxMjQ5MiwicGF0aCI6Ii8xMTMyNzY2NDEvNDMyNjE3MDE2LTFhMjAxM2VlLTRmYzgtNDcxMC1hNzBlLTk1ZTI1M2MzMzE4MS5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUwNDExJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MDQxMVQyMzAxMzJaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1lNzg1YjJjOTg1Zjc0Mzc2MzNlZjkzMzM3OWVkYzY5MDE1YjllNjBhZTYwZjI3MGQ4YjNhZmQxYjUwYzRjYzYxJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.4HB2D3VQL-ehxx6dVNqmremb3QFIlb83t0UedyI0uv8)

Oh gosh you're right. It looks like I must have had an old build 🤣 I've updated and it's the same as your screenshot - thanks for pointing it out 🙇 
author_association: CONTRIBUTOR
user: login: ihhub
created_at: 2025-04-24T06:12:46Z
body: @Districh-ru , huge thanks for this fix!
author_association: OWNER