View Issue Details

IDProjectCategoryView StatusLast Update
0005144NoesisGUIC++ SDKpublic2026-07-22 11:47
ReporterFrancoisRecisio Assigned Tojsantos  
PrioritynormalSeverityminor 
Status assignedResolutionopen 
Product Version3.2.13 
Target Version3.2.14 
Summary0005144: Some texts contain empty runs
Description

I wanted to extract the runs from a text. It works well, but sometimes I have noticed that empty runs (with a width of 0) are added, for example before non-Latin characters.
To make it easier for you to understand and reproduce, I have isolated the code in a patch.
All you need to do is compile the XamlPlayer and open the XAML I have provided. The empty runs are shown in blue.
Thanks!

Attached Files
EmptyRun.xaml (592 bytes)   
<StackPanel
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Background="White"
  TextElement.FontSize="30"
  TextElement.Foreground="Black">
  <TextBlock Margin="30" Text="こんにちは世界 안녕하세요, 세상 Hello world 你好世界 你好世界" HorizontalAlignment="Center" VerticalAlignment="Center" />
  <TextBlock Margin="30" Text="こんにちは世界 안녕하세요 세상 Hello world 你好世界 你好世界" HorizontalAlignment="Center" VerticalAlignment="Center" />
</StackPanel>
EmptyRun.xaml (592 bytes)   
emptyrun.patch (5,751 bytes)   
Index: Src/Packages/Drawing/VGL/Inc/NsDrawing/VGLTextLayout.h
===================================================================
--- Src/Packages/Drawing/VGL/Inc/NsDrawing/VGLTextLayout.h	(revision 17620)
+++ Src/Packages/Drawing/VGL/Inc/NsDrawing/VGLTextLayout.h	(working copy)
@@ -313,6 +313,10 @@
     /// Unserializes from the specified memory buffer
     void Unserialize(const char* buffer);
 
+#if NS_TEXT_SHAPING
+    const Vector<GlyphRun, 1>& kfeGetRuns() const { return mRuns; }
+#endif
+
     /// Cleanup operations on internal resources
     static void Finalize();
 
Index: Src/Packages/Drawing/VGL/Src/ShapingFull.h
===================================================================
--- Src/Packages/Drawing/VGL/Src/ShapingFull.h	(revision 17620)
+++ Src/Packages/Drawing/VGL/Src/ShapingFull.h	(working copy)
@@ -1250,7 +1250,10 @@
     float& x, float& y)
 {
     // This function expect a grapheme index, not a character index
-    NS_ASSERT(grapheme < layout->mNumGraphemes);
+    if (!(grapheme < layout->mNumGraphemes))
+    {
+        return;
+    }
 
     // Glyphs -> Display ordering
     // ShapingGlyphs -> Logical ordering
Index: Src/Packages/Gui/Core/Include/NsGui/FormattedText.h
===================================================================
--- Src/Packages/Gui/Core/Include/NsGui/FormattedText.h	(revision 17620)
+++ Src/Packages/Gui/Core/Include/NsGui/FormattedText.h	(working copy)
@@ -56,6 +56,18 @@
     float baseline;
 };
 
+////////////////////////////////////////////////////////////////////////////////////////////////////
+#if NS_TEXT_SHAPING
+struct kfeRunInfo
+{
+    float left;
+    float width;
+
+    float center() const { return left + width * 0.5f; }
+    float right() const { return left + width; }
+};
+#endif
+
 NS_WARNING_PUSH
 NS_MSVC_WARNING_DISABLE(4251 4275)
 
@@ -164,6 +176,10 @@
     bool IsInitialized() const;
     void Init();
 
+#if NS_TEXT_SHAPING
+    Vector<kfeRunInfo> kfeComputeRunInfos() const;
+#endif
+
     NS_IMPLEMENT_INTERFACE_FIXUP
 
 private:
Index: Src/Packages/Gui/Core/Src/FormattedText.cpp
===================================================================
--- Src/Packages/Gui/Core/Src/FormattedText.cpp	(revision 17620)
+++ Src/Packages/Gui/Core/Src/FormattedText.cpp	(working copy)
@@ -997,7 +997,30 @@
     return brushIndex == -1 || mBrushes[brushIndex]->IsTransparent();
 }
 
+#if NS_TEXT_SHAPING
+
 ////////////////////////////////////////////////////////////////////////////////////////////////////
+Vector<kfeRunInfo> FormattedText::kfeComputeRunInfos() const
+{
+    Vector<kfeRunInfo> output;
+
+    const Vector<GlyphRun, 1>& glyphRuns = mTextLayout->kfeGetRuns();
+    output.Reserve(glyphRuns.Size());
+    for (const GlyphRun& glyphRun : glyphRuns)
+    {
+        float left, right, y;
+        mTextLayout->HitTestTextPosition(glyphRun.firstRenderGlyph, false, left, y);
+        mTextLayout->HitTestTextPosition(glyphRun.firstRenderGlyph + glyphRun.numRenderGlyphs - 1, true, right, y);
+
+        output.EmplaceBack(kfeRunInfo{ left, right - left });
+    }
+
+    return output;
+}
+
+#endif
+
+////////////////////////////////////////////////////////////////////////////////////////////////////
 NS_BEGIN_COLD_REGION
 
 NS_IMPLEMENT_REFLECTION(FormattedText)
Index: Src/Packages/Gui/Core/Src/TextBlock.cpp
===================================================================
--- Src/Packages/Gui/Core/Src/TextBlock.cpp	(revision 17620)
+++ Src/Packages/Gui/Core/Src/TextBlock.cpp	(working copy)
@@ -20,8 +20,10 @@
 #include <NsGui/DrawingContext.h>
 #include <NsGui/FormattedText.h>
 #include <NsGui/RectangleGeometry.h>
+#include <NsGui/SolidColorBrush.h>
 #include <NsGui/MatrixTransform.h>
 #include <NsGui/Typography.h>
+#include <NsGui/Pen.h>
 #include <NsCore/UTF8.h>
 #include <NsCore/String.h>
 #include <NsCore/ReflectionImplement.h>
@@ -31,6 +33,7 @@
 
 #include "LayoutFlags.h"
 
+Noesis::Ptr<Noesis::Pen> pen;
 
 #undef DrawText
 
@@ -42,6 +45,7 @@
 #include <NsCore/HashMap.h>
 static HashSet<TextBlock*> gRegisteredTexts;
 #endif
+#include <NsGui/Brushes.h>
 
 ////////////////////////////////////////////////////////////////////////////////////////////////////
 enum TextFlags
@@ -75,6 +79,11 @@
 
     gRegisteredTexts.Insert(this);
   #endif
+
+
+    pen = Noesis::MakePtr<Noesis::Pen>();
+    pen->SetBrush(Noesis::Brushes::Black());
+    pen->SetThickness(1);
 }
 
 ////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -97,6 +106,11 @@
 
     gRegisteredTexts.Insert(this);
   #endif
+
+
+    pen = Noesis::MakePtr<Noesis::Pen>();
+    pen->SetBrush(Noesis::Brushes::Black());
+    pen->SetThickness(1);
 }
 
 ////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -701,6 +715,21 @@
         }
 
         const Thickness& padding = GetPadding();
+
+
+        int i = 0;
+        auto boup = mFormattedText->kfeComputeRunInfos();
+        for (auto kfe_run_info : boup)
+        {
+            context->DrawRectangle(
+                kfe_run_info.width >= 5.0f ? (i % 2 == 0 ? Noesis::Brushes::Red() : Noesis::Brushes::Green()) : Noesis::Brushes::Blue(),
+                pen,
+                Noesis::Rect(Noesis::Point(padding.left, padding.top) + Noesis::Point(kfe_run_info.left, kfe_run_info.width >= 5.0f ? 0 : 10.0f), Noesis::Size(Noesis::Max(kfe_run_info.width, 5.0f), textSize.height)));
+            i++;
+        }
+
+
+
         context->DrawText(mFormattedText, Point(padding.left, padding.top));
 
         if (NS_UNLIKELY(rtl))
emptyrun.patch (5,751 bytes)   
emptyrun.png (27,042 bytes)   
emptyrun.png (27,042 bytes)   
PlatformAny

Activities

Issue History

Date Modified Username Field Change
2026-07-22 11:37 FrancoisRecisio New Issue
2026-07-22 11:37 FrancoisRecisio File Added: EmptyRun.xaml
2026-07-22 11:37 FrancoisRecisio File Added: emptyrun.patch
2026-07-22 11:37 FrancoisRecisio File Added: emptyrun.png
2026-07-22 11:46 jsantos Assigned To => jsantos
2026-07-22 11:46 jsantos Status new => assigned
2026-07-22 11:47 jsantos Product Version 3.2.14 => 3.2.13
2026-07-22 11:47 jsantos Target Version => 3.2.14