diff --git a/Client/UI/CairoFont.cs b/Client/UI/CairoFont.cs
index 1cf69379..ef53b4bd 100644
--- a/Client/UI/CairoFont.cs
+++ b/Client/UI/CairoFont.cs
@@ -24,7 +24,8 @@ public class CairoFont : FontConfig, IDisposable
public double LineHeightMultiplier = 1f;
- FontOptions CairoFontOptions;
+ FontOptions CairoFontOptions;
+ const string ArabicFallbackFontName = "Tahoma";
public FontSlant Slant = FontSlant.Normal;
@@ -213,11 +214,16 @@ public CairoFont WithFont(string fontname)
/// Sets up the context. Must be executed in the main thread, as it is not thread safe.
///
/// The context to set up the CairoFont with.
- public void SetupContext(Context ctx)
- {
- ctx.SetFontSize(GuiElement.scaled(UnscaledFontsize));
- ctx.SelectFontFace(Fontname, Slant, FontWeight);
- CairoFontOptions = new FontOptions();
+ public void SetupContext(Context ctx)
+ {
+ SetupContext(ctx, null);
+ }
+
+ public void SetupContext(Context ctx, string text)
+ {
+ ctx.SetFontSize(GuiElement.scaled(UnscaledFontsize));
+ ctx.SelectFontFace(GetFontNameForText(text), Slant, GetFontWeightForText(text));
+ CairoFontOptions = new FontOptions();
// Antialias.Best does not work on Linux it completely borks the font
CairoFontOptions.Antialias = Antialias.Subpixel;
@@ -233,29 +239,61 @@ public void SetupContext(Context ctx)
{
ctx.SetSourceRGBA(Color[0], Color[1], Color[2], Color[3]);
}
- }
- }
+ }
+ }
+
+ string GetFontNameForText(string text)
+ {
+ if (!ComplexTextLayout.RequiresRightToLeftLayout(text))
+ {
+ return Fontname;
+ }
+
+ return ArabicFallbackFontName;
+ }
+
+ FontWeight GetFontWeightForText(string text)
+ {
+ if (!ComplexTextLayout.RequiresRightToLeftLayout(text))
+ {
+ return FontWeight;
+ }
+
+ return Cairo.FontWeight.Normal;
+ }
///
/// Gets the font's extents.
///
/// The FontExtents for this particular font.
- public FontExtents GetFontExtents()
- {
- SetupContext(FontMeasuringContext);
- return FontMeasuringContext.FontExtents;
- }
+ public FontExtents GetFontExtents()
+ {
+ SetupContext(FontMeasuringContext);
+ return FontMeasuringContext.FontExtents;
+ }
+
+ public FontExtents GetFontExtents(string text)
+ {
+ SetupContext(FontMeasuringContext, text);
+ return FontMeasuringContext.FontExtents;
+ }
///
/// Gets the extents of the text.
///
/// The text to extend.
/// The Text extends for this font with this text.
- public TextExtents GetTextExtents(string text)
- {
- SetupContext(FontMeasuringContext);
- return FontMeasuringContext.TextExtents(text);
- }
+ public TextExtents GetTextExtents(string text)
+ {
+ SetupContext(FontMeasuringContext, text);
+
+ if (CairoGlyphLayout.TryCreateGlyphs(FontMeasuringContext, text, 0, 0, out Glyph[] glyphs))
+ {
+ return FontMeasuringContext.GlyphExtents(glyphs);
+ }
+
+ return FontMeasuringContext.TextExtents(ComplexTextLayout.PrepareForRendering(text));
+ }
///
/// Clone function. Creates a duplicate of this Cairofont.
diff --git a/Client/UI/CairoGlyphLayout.cs b/Client/UI/CairoGlyphLayout.cs
new file mode 100644
index 00000000..619c0688
--- /dev/null
+++ b/Client/UI/CairoGlyphLayout.cs
@@ -0,0 +1,142 @@
+using System;
+using System.Runtime.InteropServices;
+using Cairo;
+
+#nullable disable
+
+namespace Vintagestory.API.Client
+{
+ internal static class CairoGlyphLayout
+ {
+ // The native cairo glyph conversion used by the shipped runtime resolves glyph indices,
+ // but it does not perform Arabic shaping. Enabling it leaves right-to-left text disconnected.
+ const bool NativeGlyphLayoutEnabled = false;
+
+ [StructLayout(LayoutKind.Sequential)]
+ struct NativeGlyph32
+ {
+ public uint Index;
+ public double X;
+ public double Y;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ struct NativeGlyph64
+ {
+ public ulong Index;
+ public double X;
+ public double Y;
+ }
+
+ [DllImport("libcairo-2.dll", CallingConvention = CallingConvention.Cdecl)]
+ static extern int cairo_scaled_font_text_to_glyphs(
+ IntPtr scaledFont,
+ double x,
+ double y,
+ [MarshalAs(UnmanagedType.LPUTF8Str)] string utf8,
+ int utf8Len,
+ out IntPtr glyphs,
+ out int numGlyphs,
+ IntPtr clusters,
+ IntPtr numClusters,
+ IntPtr clusterFlags);
+
+ [DllImport("libcairo-2.dll", CallingConvention = CallingConvention.Cdecl)]
+ static extern void cairo_glyph_free(IntPtr glyphs);
+
+ public static bool TryCreateGlyphs(Context ctx, string text, double originX, double originY, out Glyph[] glyphs)
+ {
+ glyphs = null;
+
+ if (!NativeGlyphLayoutEnabled)
+ {
+ return false;
+ }
+
+ if (ctx == null || string.IsNullOrEmpty(text) || !ComplexTextLayout.RequiresRightToLeftLayout(text))
+ {
+ return false;
+ }
+
+ string preparedText = ComplexTextLayout.PrepareForRendering(text);
+ if (string.IsNullOrEmpty(preparedText))
+ {
+ return false;
+ }
+
+ IntPtr nativeGlyphs = IntPtr.Zero;
+
+ try
+ {
+ int status = cairo_scaled_font_text_to_glyphs(
+ ctx.GetScaledFont().Handle,
+ originX,
+ originY,
+ preparedText,
+ -1,
+ out nativeGlyphs,
+ out int numGlyphs,
+ IntPtr.Zero,
+ IntPtr.Zero,
+ IntPtr.Zero
+ );
+
+ if (status != 0 || nativeGlyphs == IntPtr.Zero || numGlyphs <= 0)
+ {
+ return false;
+ }
+
+ glyphs = CopyGlyphs(nativeGlyphs, numGlyphs);
+ return glyphs.Length > 0;
+ }
+ catch
+ {
+ glyphs = null;
+ return false;
+ }
+ finally
+ {
+ if (nativeGlyphs != IntPtr.Zero)
+ {
+ cairo_glyph_free(nativeGlyphs);
+ }
+ }
+ }
+
+ static Glyph[] CopyGlyphs(IntPtr nativeGlyphs, int numGlyphs)
+ {
+ Glyph[] glyphs = new Glyph[numGlyphs];
+
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ int glyphSize = Marshal.SizeOf();
+ for (int i = 0; i < numGlyphs; i++)
+ {
+ NativeGlyph32 nativeGlyph = Marshal.PtrToStructure(nativeGlyphs + i * glyphSize);
+ glyphs[i] = new Glyph
+ {
+ Index = nativeGlyph.Index,
+ X = nativeGlyph.X,
+ Y = nativeGlyph.Y
+ };
+ }
+ }
+ else
+ {
+ int glyphSize = Marshal.SizeOf();
+ for (int i = 0; i < numGlyphs; i++)
+ {
+ NativeGlyph64 nativeGlyph = Marshal.PtrToStructure(nativeGlyphs + i * glyphSize);
+ glyphs[i] = new Glyph
+ {
+ Index = (long)nativeGlyph.Index,
+ X = nativeGlyph.X,
+ Y = nativeGlyph.Y
+ };
+ }
+ }
+
+ return glyphs;
+ }
+ }
+}
diff --git a/Client/UI/ComplexTextLayout.cs b/Client/UI/ComplexTextLayout.cs
new file mode 100644
index 00000000..53b93884
--- /dev/null
+++ b/Client/UI/ComplexTextLayout.cs
@@ -0,0 +1,639 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text;
+
+#nullable disable
+
+namespace Vintagestory.API.Client
+{
+ internal static class ComplexTextLayout
+ {
+ enum TextDirection
+ {
+ Neutral,
+ LeftToRight,
+ RightToLeft
+ }
+
+ readonly struct ArabicForms
+ {
+ public readonly char Isolated;
+ public readonly char Final;
+ public readonly char Initial;
+ public readonly char Medial;
+
+ public ArabicForms(char isolated, char final, char initial = '\0', char medial = '\0')
+ {
+ Isolated = isolated;
+ Final = final;
+ Initial = initial;
+ Medial = medial;
+ }
+
+ public bool ConnectsToPrevious => Final != '\0' || Medial != '\0';
+ public bool ConnectsToNext => Initial != '\0' || Medial != '\0';
+
+ public char GetShapedChar(bool connectsToPrevious, bool connectsToNext)
+ {
+ if (connectsToPrevious && connectsToNext && Medial != '\0') return Medial;
+ if (connectsToPrevious && Final != '\0') return Final;
+ if (connectsToNext && Initial != '\0') return Initial;
+ return Isolated;
+ }
+ }
+
+ readonly struct DirectionalRun
+ {
+ public readonly TextDirection Direction;
+ public readonly List Clusters;
+
+ public DirectionalRun(TextDirection direction, List clusters)
+ {
+ Direction = direction;
+ Clusters = clusters;
+ }
+ }
+
+ static readonly Dictionary ArabicPresentationForms = new Dictionary
+ {
+ ['\u0621'] = new ArabicForms('\uFE80', '\0'),
+ ['\u0622'] = new ArabicForms('\uFE81', '\uFE82'),
+ ['\u0623'] = new ArabicForms('\uFE83', '\uFE84'),
+ ['\u0624'] = new ArabicForms('\uFE85', '\uFE86'),
+ ['\u0625'] = new ArabicForms('\uFE87', '\uFE88'),
+ ['\u0626'] = new ArabicForms('\uFE89', '\uFE8A', '\uFE8B', '\uFE8C'),
+ ['\u0627'] = new ArabicForms('\uFE8D', '\uFE8E'),
+ ['\u0628'] = new ArabicForms('\uFE8F', '\uFE90', '\uFE91', '\uFE92'),
+ ['\u0629'] = new ArabicForms('\uFE93', '\uFE94'),
+ ['\u062A'] = new ArabicForms('\uFE95', '\uFE96', '\uFE97', '\uFE98'),
+ ['\u062B'] = new ArabicForms('\uFE99', '\uFE9A', '\uFE9B', '\uFE9C'),
+ ['\u062C'] = new ArabicForms('\uFE9D', '\uFE9E', '\uFE9F', '\uFEA0'),
+ ['\u062D'] = new ArabicForms('\uFEA1', '\uFEA2', '\uFEA3', '\uFEA4'),
+ ['\u062E'] = new ArabicForms('\uFEA5', '\uFEA6', '\uFEA7', '\uFEA8'),
+ ['\u062F'] = new ArabicForms('\uFEA9', '\uFEAA'),
+ ['\u0630'] = new ArabicForms('\uFEAB', '\uFEAC'),
+ ['\u0631'] = new ArabicForms('\uFEAD', '\uFEAE'),
+ ['\u0632'] = new ArabicForms('\uFEAF', '\uFEB0'),
+ ['\u0633'] = new ArabicForms('\uFEB1', '\uFEB2', '\uFEB3', '\uFEB4'),
+ ['\u0634'] = new ArabicForms('\uFEB5', '\uFEB6', '\uFEB7', '\uFEB8'),
+ ['\u0635'] = new ArabicForms('\uFEB9', '\uFEBA', '\uFEBB', '\uFEBC'),
+ ['\u0636'] = new ArabicForms('\uFEBD', '\uFEBE', '\uFEBF', '\uFEC0'),
+ ['\u0637'] = new ArabicForms('\uFEC1', '\uFEC2', '\uFEC3', '\uFEC4'),
+ ['\u0638'] = new ArabicForms('\uFEC5', '\uFEC6', '\uFEC7', '\uFEC8'),
+ ['\u0639'] = new ArabicForms('\uFEC9', '\uFECA', '\uFECB', '\uFECC'),
+ ['\u063A'] = new ArabicForms('\uFECD', '\uFECE', '\uFECF', '\uFED0'),
+ ['\u0640'] = new ArabicForms('\u0640', '\u0640', '\u0640', '\u0640'),
+ ['\u0641'] = new ArabicForms('\uFED1', '\uFED2', '\uFED3', '\uFED4'),
+ ['\u0642'] = new ArabicForms('\uFED5', '\uFED6', '\uFED7', '\uFED8'),
+ ['\u0643'] = new ArabicForms('\uFED9', '\uFEDA', '\uFEDB', '\uFEDC'),
+ ['\u0644'] = new ArabicForms('\uFEDD', '\uFEDE', '\uFEDF', '\uFEE0'),
+ ['\u0645'] = new ArabicForms('\uFEE1', '\uFEE2', '\uFEE3', '\uFEE4'),
+ ['\u0646'] = new ArabicForms('\uFEE5', '\uFEE6', '\uFEE7', '\uFEE8'),
+ ['\u0647'] = new ArabicForms('\uFEE9', '\uFEEA', '\uFEEB', '\uFEEC'),
+ ['\u0648'] = new ArabicForms('\uFEED', '\uFEEE'),
+ ['\u0649'] = new ArabicForms('\uFEEF', '\uFEF0'),
+ ['\u064A'] = new ArabicForms('\uFEF1', '\uFEF2', '\uFEF3', '\uFEF4'),
+ ['\u0671'] = new ArabicForms('\uFB50', '\uFB51'),
+ ['\u067E'] = new ArabicForms('\uFB56', '\uFB57', '\uFB58', '\uFB59'),
+ ['\u0686'] = new ArabicForms('\uFB7A', '\uFB7B', '\uFB7C', '\uFB7D'),
+ ['\u0688'] = new ArabicForms('\uFB88', '\uFB89'),
+ ['\u0691'] = new ArabicForms('\uFB8C', '\uFB8D'),
+ ['\u0698'] = new ArabicForms('\uFB8A', '\uFB8B'),
+ ['\u06A4'] = new ArabicForms('\uFB6A', '\uFB6B', '\uFB6C', '\uFB6D'),
+ ['\u06A9'] = new ArabicForms('\uFB8E', '\uFB8F', '\uFB90', '\uFB91'),
+ ['\u06AF'] = new ArabicForms('\uFB92', '\uFB93', '\uFB94', '\uFB95'),
+ ['\u06BA'] = new ArabicForms('\uFB9E', '\uFB9F'),
+ ['\u06BE'] = new ArabicForms('\uFBAA', '\uFBAB', '\uFBAC', '\uFBAD'),
+ ['\u06C1'] = new ArabicForms('\uFBA6', '\uFBA7', '\uFBA8', '\uFBA9'),
+ ['\u06CC'] = new ArabicForms('\uFBFC', '\uFBFD', '\uFBFE', '\uFBFF'),
+ ['\u06D2'] = new ArabicForms('\uFBAE', '\uFBAF')
+ };
+
+ static readonly Dictionary MirroredChars = new Dictionary
+ {
+ ['('] = ')',
+ [')'] = '(',
+ ['['] = ']',
+ [']'] = '[',
+ ['{'] = '}',
+ ['}'] = '{',
+ ['<'] = '>',
+ ['>'] = '<'
+ };
+
+ static readonly HashSet LtrFriendlyNeutrals = new HashSet
+ {
+ '.', ',', ':', ';', '/', '\\', '-', '_', '+', '@', '#', '&', '=', '%', '*'
+ };
+
+ public static string PrepareForRendering(string text)
+ {
+ if (string.IsNullOrEmpty(text) || !ContainsRightToLeftText(text))
+ {
+ return text;
+ }
+
+ if (ContainsPresentationForms(text) || ContainsDirectionalControlCharacters(text))
+ {
+ return text;
+ }
+
+ StringBuilder builder = new StringBuilder(text.Length);
+ int lineStart = 0;
+
+ while (lineStart < text.Length)
+ {
+ int lineEnd = lineStart;
+ while (lineEnd < text.Length && text[lineEnd] != '\r' && text[lineEnd] != '\n')
+ {
+ lineEnd++;
+ }
+
+ builder.Append(PrepareLine(text.Substring(lineStart, lineEnd - lineStart), true));
+
+ if (lineEnd < text.Length)
+ {
+ if (text[lineEnd] == '\r' && lineEnd + 1 < text.Length && text[lineEnd + 1] == '\n')
+ {
+ builder.Append("\r\n");
+ lineStart = lineEnd + 2;
+ }
+ else
+ {
+ builder.Append(text[lineEnd]);
+ lineStart = lineEnd + 1;
+ }
+ }
+ else
+ {
+ lineStart = lineEnd;
+ }
+ }
+
+ return builder.ToString();
+ }
+
+ public static string PrepareForGlyphRendering(string text)
+ {
+ if (string.IsNullOrEmpty(text) || !ContainsRightToLeftText(text))
+ {
+ return text;
+ }
+
+ if (ContainsDirectionalControlCharacters(text))
+ {
+ return text;
+ }
+
+ StringBuilder builder = new StringBuilder(text.Length);
+ int lineStart = 0;
+
+ while (lineStart < text.Length)
+ {
+ int lineEnd = lineStart;
+ while (lineEnd < text.Length && text[lineEnd] != '\r' && text[lineEnd] != '\n')
+ {
+ lineEnd++;
+ }
+
+ builder.Append(PrepareLine(text.Substring(lineStart, lineEnd - lineStart), false));
+
+ if (lineEnd < text.Length)
+ {
+ if (text[lineEnd] == '\r' && lineEnd + 1 < text.Length && text[lineEnd + 1] == '\n')
+ {
+ builder.Append("\r\n");
+ lineStart = lineEnd + 2;
+ }
+ else
+ {
+ builder.Append(text[lineEnd]);
+ lineStart = lineEnd + 1;
+ }
+ }
+ else
+ {
+ lineStart = lineEnd;
+ }
+ }
+
+ return builder.ToString();
+ }
+
+ public static bool RequiresRightToLeftLayout(string text)
+ {
+ return !string.IsNullOrEmpty(text) && ContainsRightToLeftText(text);
+ }
+
+ static string PrepareLine(string line, bool shapeArabic)
+ {
+ if (string.IsNullOrEmpty(line) || !ContainsRightToLeftText(line))
+ {
+ return line;
+ }
+
+ string shapedLine = shapeArabic ? ShapeArabic(line) : line;
+ List clusters = CreateClusters(shapedLine);
+ if (clusters.Count == 0)
+ {
+ return shapedLine;
+ }
+
+ TextDirection baseDirection = GetBaseDirection(line);
+ List runs = CreateRuns(clusters, baseDirection);
+
+ StringBuilder builder = new StringBuilder(shapedLine.Length);
+
+ if (baseDirection == TextDirection.RightToLeft)
+ {
+ for (int i = runs.Count - 1; i >= 0; i--)
+ {
+ AppendRun(builder, runs[i]);
+ }
+ }
+ else
+ {
+ for (int i = 0; i < runs.Count; i++)
+ {
+ AppendRun(builder, runs[i]);
+ }
+ }
+
+ return builder.ToString();
+ }
+
+ static void AppendRun(StringBuilder builder, DirectionalRun run)
+ {
+ if (run.Direction == TextDirection.RightToLeft)
+ {
+ for (int i = run.Clusters.Count - 1; i >= 0; i--)
+ {
+ builder.Append(MirrorCluster(run.Clusters[i]));
+ }
+
+ return;
+ }
+
+ for (int i = 0; i < run.Clusters.Count; i++)
+ {
+ builder.Append(run.Clusters[i]);
+ }
+ }
+
+ static List CreateRuns(List clusters, TextDirection baseDirection)
+ {
+ TextDirection[] directions = new TextDirection[clusters.Count];
+
+ for (int i = 0; i < clusters.Count; i++)
+ {
+ directions[i] = ClassifyCluster(clusters[i]);
+ }
+
+ for (int i = 0; i < directions.Length; i++)
+ {
+ if (directions[i] == TextDirection.Neutral)
+ {
+ directions[i] = ResolveNeutralDirection(directions, i, baseDirection);
+ }
+ }
+
+ List runs = new List();
+ List currentClusters = new List();
+ TextDirection currentDirection = directions[0];
+
+ for (int i = 0; i < clusters.Count; i++)
+ {
+ if (currentClusters.Count > 0 && directions[i] != currentDirection)
+ {
+ runs.Add(new DirectionalRun(currentDirection, currentClusters));
+ currentClusters = new List();
+ currentDirection = directions[i];
+ }
+
+ currentClusters.Add(clusters[i]);
+ }
+
+ if (currentClusters.Count > 0)
+ {
+ runs.Add(new DirectionalRun(currentDirection, currentClusters));
+ }
+
+ return runs;
+ }
+
+ static TextDirection ClassifyCluster(string cluster)
+ {
+ if (string.IsNullOrEmpty(cluster))
+ {
+ return TextDirection.Neutral;
+ }
+
+ for (int i = 0; i < cluster.Length; i++)
+ {
+ char chr = cluster[i];
+
+ if (IsRightToLeftChar(chr))
+ {
+ return TextDirection.RightToLeft;
+ }
+
+ if (char.IsDigit(chr) || IsLeftToRightLetter(chr))
+ {
+ return TextDirection.LeftToRight;
+ }
+ }
+
+ return TextDirection.Neutral;
+ }
+
+ static TextDirection ResolveNeutralDirection(TextDirection[] directions, int index, TextDirection baseDirection)
+ {
+ TextDirection previousDirection = FindStrongDirection(directions, index - 1, -1);
+ TextDirection nextDirection = FindStrongDirection(directions, index + 1, 1);
+
+ if (previousDirection != TextDirection.Neutral && previousDirection == nextDirection)
+ {
+ return previousDirection;
+ }
+
+ if (previousDirection != TextDirection.Neutral && nextDirection == TextDirection.Neutral)
+ {
+ return previousDirection;
+ }
+
+ if (nextDirection != TextDirection.Neutral && previousDirection == TextDirection.Neutral)
+ {
+ return nextDirection;
+ }
+
+ return baseDirection == TextDirection.Neutral ? TextDirection.LeftToRight : baseDirection;
+ }
+
+ static TextDirection FindStrongDirection(TextDirection[] directions, int index, int step)
+ {
+ while (index >= 0 && index < directions.Length)
+ {
+ if (directions[index] != TextDirection.Neutral)
+ {
+ return directions[index];
+ }
+
+ index += step;
+ }
+
+ return TextDirection.Neutral;
+ }
+
+ static List CreateClusters(string text)
+ {
+ List clusters = new List();
+ StringBuilder cluster = new StringBuilder();
+
+ for (int i = 0; i < text.Length; i++)
+ {
+ char chr = text[i];
+
+ if (cluster.Length > 0 && IsTransparentChar(chr))
+ {
+ cluster.Append(chr);
+ continue;
+ }
+
+ if (cluster.Length > 0)
+ {
+ clusters.Add(cluster.ToString());
+ cluster.Clear();
+ }
+
+ cluster.Append(chr);
+ }
+
+ if (cluster.Length > 0)
+ {
+ clusters.Add(cluster.ToString());
+ }
+
+ return clusters;
+ }
+
+ static string MirrorCluster(string cluster)
+ {
+ if (string.IsNullOrEmpty(cluster))
+ {
+ return cluster;
+ }
+
+ char chr = cluster[0];
+ if (!MirroredChars.TryGetValue(chr, out char mirrored))
+ {
+ return cluster;
+ }
+
+ if (cluster.Length == 1)
+ {
+ return mirrored.ToString();
+ }
+
+ char[] chars = cluster.ToCharArray();
+ chars[0] = mirrored;
+ return new string(chars);
+ }
+
+ static string ShapeArabic(string text)
+ {
+ if (string.IsNullOrEmpty(text))
+ {
+ return text;
+ }
+
+ char[] sourceChars = text.ToCharArray();
+ StringBuilder builder = new StringBuilder(text.Length);
+
+ for (int i = 0; i < sourceChars.Length; i++)
+ {
+ char currentChar = sourceChars[i];
+ if (!ArabicPresentationForms.TryGetValue(currentChar, out ArabicForms currentForms))
+ {
+ builder.Append(currentChar);
+ continue;
+ }
+
+ int previousIndex = FindPreviousJoiningIndex(sourceChars, i - 1);
+ int nextIndex = FindNextJoiningIndex(sourceChars, i + 1);
+
+ bool connectsToPrevious =
+ previousIndex >= 0 &&
+ ArabicPresentationForms.TryGetValue(sourceChars[previousIndex], out ArabicForms previousForms) &&
+ previousForms.ConnectsToNext &&
+ currentForms.ConnectsToPrevious;
+
+ if (currentChar == '\u0644' && TryGetLamAlefLigature(sourceChars, i, nextIndex, connectsToPrevious, out char ligature))
+ {
+ builder.Append(ligature);
+ i = nextIndex;
+ continue;
+ }
+
+ bool connectsToNext =
+ nextIndex >= 0 &&
+ ArabicPresentationForms.TryGetValue(sourceChars[nextIndex], out ArabicForms nextForms) &&
+ currentForms.ConnectsToNext &&
+ nextForms.ConnectsToPrevious;
+
+ builder.Append(currentForms.GetShapedChar(connectsToPrevious, connectsToNext));
+ }
+
+ return builder.ToString();
+ }
+
+ static bool TryGetLamAlefLigature(char[] chars, int lamIndex, int nextIndex, bool connectsToPrevious, out char ligature)
+ {
+ ligature = '\0';
+
+ if (nextIndex != lamIndex + 1)
+ {
+ return false;
+ }
+
+ switch (chars[nextIndex])
+ {
+ case '\u0622':
+ ligature = connectsToPrevious ? '\uFEF6' : '\uFEF5';
+ return true;
+ case '\u0623':
+ ligature = connectsToPrevious ? '\uFEF8' : '\uFEF7';
+ return true;
+ case '\u0625':
+ ligature = connectsToPrevious ? '\uFEFA' : '\uFEF9';
+ return true;
+ case '\u0627':
+ ligature = connectsToPrevious ? '\uFEFC' : '\uFEFB';
+ return true;
+ }
+
+ return false;
+ }
+
+ static int FindPreviousJoiningIndex(char[] chars, int index)
+ {
+ while (index >= 0)
+ {
+ if (!IsTransparentChar(chars[index]))
+ {
+ return index;
+ }
+
+ index--;
+ }
+
+ return -1;
+ }
+
+ static int FindNextJoiningIndex(char[] chars, int index)
+ {
+ while (index < chars.Length)
+ {
+ if (!IsTransparentChar(chars[index]))
+ {
+ return index;
+ }
+
+ index++;
+ }
+
+ return -1;
+ }
+
+ static TextDirection GetBaseDirection(string text)
+ {
+ for (int i = 0; i < text.Length; i++)
+ {
+ char chr = text[i];
+
+ if (IsRightToLeftChar(chr))
+ {
+ return TextDirection.RightToLeft;
+ }
+
+ if (IsLeftToRightLetter(chr))
+ {
+ return TextDirection.LeftToRight;
+ }
+ }
+
+ return TextDirection.Neutral;
+ }
+
+ static bool ContainsRightToLeftText(string text)
+ {
+ for (int i = 0; i < text.Length; i++)
+ {
+ if (IsRightToLeftChar(text[i]))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ static bool IsTransparentChar(char chr)
+ {
+ UnicodeCategory category = CharUnicodeInfo.GetUnicodeCategory(chr);
+ return category == UnicodeCategory.NonSpacingMark
+ || category == UnicodeCategory.SpacingCombiningMark
+ || category == UnicodeCategory.EnclosingMark;
+ }
+
+ static bool IsLeftToRightLetter(char chr)
+ {
+ return char.IsLetter(chr) && !IsRightToLeftChar(chr);
+ }
+
+ static bool ContainsPresentationForms(string text)
+ {
+ for (int i = 0; i < text.Length; i++)
+ {
+ char chr = text[i];
+ if ((chr >= '\uFB50' && chr <= '\uFDFF') || (chr >= '\uFE70' && chr <= '\uFEFF'))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ static bool ContainsDirectionalControlCharacters(string text)
+ {
+ for (int i = 0; i < text.Length; i++)
+ {
+ switch (text[i])
+ {
+ case '\u200E':
+ case '\u200F':
+ case '\u202A':
+ case '\u202B':
+ case '\u202C':
+ case '\u202D':
+ case '\u202E':
+ case '\u2066':
+ case '\u2067':
+ case '\u2068':
+ case '\u2069':
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ static bool IsRightToLeftChar(char chr)
+ {
+ return
+ (chr >= '\u0590' && chr <= '\u08FF') ||
+ (chr >= '\uFB1D' && chr <= '\uFDFF') ||
+ (chr >= '\uFE70' && chr <= '\uFEFF');
+ }
+ }
+}
diff --git a/Client/UI/Elements/Impl/Interactive/Controls/GuiElementHorizontalTabs.cs b/Client/UI/Elements/Impl/Interactive/Controls/GuiElementHorizontalTabs.cs
index b5e0e713..f81680c8 100644
--- a/Client/UI/Elements/Impl/Interactive/Controls/GuiElementHorizontalTabs.cs
+++ b/Client/UI/Elements/Impl/Interactive/Controls/GuiElementHorizontalTabs.cs
@@ -98,21 +98,19 @@ public override void ComposeTextElements(Context ctxStatic, ImageSurface surface
ImageSurface surface = new ImageSurface(Format.Argb32, (int)Bounds.InnerWidth + 1, (int)Bounds.InnerHeight + 1);
Context ctx = new Context(surface);
- Font.SetupContext(ctx);
-
- fontHeight = (float)Font.GetFontExtents().Height;
+ fontHeight = (float)Font.GetFontExtents(tabs.Length > 0 ? tabs[0].Name : null).Height;
double radius = scaled(1);
double spacing = scaled(unscaledTabSpacing);
double padding = scaled(unscaledTabPadding);
- totalWidth = 0;
-
- for (int i = 0; i < tabs.Length; i++)
- {
- tabWidths[i] = (int)(ctx.TextExtents(tabs[i].Name).Width + 2 * padding + 1);
- totalWidth += spacing + tabWidths[i];
- }
+ totalWidth = 0;
+
+ for (int i = 0; i < tabs.Length; i++)
+ {
+ tabWidths[i] = (int)(Font.GetTextExtents(tabs[i].Name).Width + 2 * padding + 1);
+ totalWidth += spacing + tabWidths[i];
+ }
ctx.Dispose();
surface.Dispose();
@@ -140,12 +138,12 @@ public override void ComposeTextElements(Context ctxStatic, ImageSurface surface
ShadePath(ctx, 2);
- if (AlarmTabs)
- {
- notifyFont.SetupContext(ctx);
- } else {
- Font.SetupContext(ctx);
- }
+ if (AlarmTabs)
+ {
+ notifyFont.SetupContext(ctx, tabs[i].Name);
+ } else {
+ Font.SetupContext(ctx, tabs[i].Name);
+ }
DrawTextLineAt(ctx, tabs[i].Name, xpos + padding, (surface.Height - fontHeight) / 2);
@@ -212,13 +210,13 @@ private void ComposeOverlays(bool isNotifyTabs = false)
ShadePath(ctx, 2);
- if (isNotifyTabs)
- {
- notifyFont.SetupContext(ctx);
- } else
- {
- selectedFont.SetupContext(ctx);
- }
+ if (isNotifyTabs)
+ {
+ notifyFont.SetupContext(ctx, tabs[i].Name);
+ } else
+ {
+ selectedFont.SetupContext(ctx, tabs[i].Name);
+ }
ctx.Operator = Operator.Clear;
ctx.Rectangle(0, surface.Height - 1, surface.Width, 1);
diff --git a/Client/UI/Elements/Impl/Interactive/Controls/GuiElementVerticalTabs.cs b/Client/UI/Elements/Impl/Interactive/Controls/GuiElementVerticalTabs.cs
index dc21d6a8..ac1e3f7d 100644
--- a/Client/UI/Elements/Impl/Interactive/Controls/GuiElementVerticalTabs.cs
+++ b/Client/UI/Elements/Impl/Interactive/Controls/GuiElementVerticalTabs.cs
@@ -70,17 +70,16 @@ public override void ComposeTextElements(Context ctxStatic, ImageSurface surface
double ypos = 0;
- Font.Color[3] = 0.85;
- Font.SetupContext(ctx);
- textOffsetY = (tabHeight + 1 - Font.GetFontExtents().Height) / 2;
+ Font.Color[3] = 0.85;
+ textOffsetY = (tabHeight + 1 - Font.GetFontExtents(tabs.Length > 0 ? tabs[0].Name : null).Height) / 2;
- double maxWidth = 0;
- for (int i = 0; i < tabs.Length; i++)
- {
- double w = (int)(ctx.TextExtents(tabs[i].Name).Width + 1 + 2 * padding);
-
- maxWidth = Math.Max(w, maxWidth);
- }
+ double maxWidth = 0;
+ for (int i = 0; i < tabs.Length; i++)
+ {
+ double w = (int)(Font.GetTextExtents(tabs[i].Name).Width + 1 + 2 * padding);
+
+ maxWidth = Math.Max(w, maxWidth);
+ }
for (int i = 0; i < tabs.Length; i++)
@@ -121,7 +120,7 @@ public override void ComposeTextElements(Context ctxStatic, ImageSurface surface
ShadePath(ctx, 2);
- Font.SetupContext(ctx);
+ Font.SetupContext(ctx, tabs[i].Name);
DrawTextLineAt(ctx, tabs[i].Name, xpos - (Right ? 0 : tabWidths[i]) + padding, ypos + textOffsetY);
@@ -195,7 +194,7 @@ private void ComposeOverlays()
ctx.Stroke();
- selectedFont.SetupContext(ctx);
+ selectedFont.SetupContext(ctx, tabs[i].Name);
DrawTextLineAt(ctx, tabs[i].Name, padding+2, textOffsetY);
@@ -383,4 +382,4 @@ public static GuiElementVerticalTabs GetVerticalTab(this GuiComposer composer, s
return (GuiElementVerticalTabs)composer.GetElement(key);
}
}
-}
\ No newline at end of file
+}
diff --git a/Client/UI/TextDrawUtil.cs b/Client/UI/TextDrawUtil.cs
index c1f57c6a..6cc4a4b1 100644
--- a/Client/UI/TextDrawUtil.cs
+++ b/Client/UI/TextDrawUtil.cs
@@ -95,11 +95,21 @@ public LineRectangled() : base()
- public class TextDrawUtil
- {
- int caretPos = 0;
- bool gotLinebreak = false;
- bool gotSpace = false;
+ public class TextDrawUtil
+ {
+ int caretPos = 0;
+ bool gotLinebreak = false;
+ bool gotSpace = false;
+
+ static double GetRenderedTextWidth(Context ctx, string text)
+ {
+ if (CairoGlyphLayout.TryCreateGlyphs(ctx, text, 0, 0, out Glyph[] glyphs))
+ {
+ return ctx.GlyphExtents(glyphs).Width;
+ }
+
+ return ctx.TextExtents(ComplexTextLayout.PrepareForRendering(text)).Width;
+ }
#region Shorthand methods for simple box constrained multiline text
@@ -198,13 +208,13 @@ public double GetLineHeight(CairoFont font)
/// The path for the text.
/// The height of the line
/// The number of lines.
- public int GetQuantityTextLines(CairoFont font, string text, EnumLinebreakBehavior linebreak, TextFlowPath[] flowPath, double lineY = 0)
- {
- if (text == null || text.Length == 0) return 0;
-
- ImageSurface surface = new ImageSurface(Format.Argb32, 1, 1);
- Context ctx = new Context(surface);
- font.SetupContext(ctx);
+ public int GetQuantityTextLines(CairoFont font, string text, EnumLinebreakBehavior linebreak, TextFlowPath[] flowPath, double lineY = 0)
+ {
+ if (text == null || text.Length == 0) return 0;
+
+ ImageSurface surface = new ImageSurface(Format.Argb32, 1, 1);
+ Context ctx = new Context(surface);
+ font.SetupContext(ctx, text);
int quantityLines = Lineize(ctx, text, linebreak, flowPath, 0, lineY, font.LineHeightMultiplier).Length;
@@ -240,13 +250,13 @@ public double GetMultilineTextHeight(CairoFont font, string text, EnumLinebreakB
/// The offset start position for Y
///
/// The text broken up into lines.
- public TextLine[] Lineize(CairoFont font, string fulltext, EnumLinebreakBehavior linebreak, TextFlowPath[] flowPath, double startOffsetX = 0, double startY = 0, bool keepLinebreakChar = false)
- {
- if (fulltext == null || fulltext.Length == 0) return Array.Empty();
-
- ImageSurface surface = new ImageSurface(Format.Argb32, 1, 1);
- Context ctx = new Context(surface);
- font.SetupContext(ctx);
+ public TextLine[] Lineize(CairoFont font, string fulltext, EnumLinebreakBehavior linebreak, TextFlowPath[] flowPath, double startOffsetX = 0, double startY = 0, bool keepLinebreakChar = false)
+ {
+ if (fulltext == null || fulltext.Length == 0) return Array.Empty();
+
+ ImageSurface surface = new ImageSurface(Format.Argb32, 1, 1);
+ Context ctx = new Context(surface);
+ font.SetupContext(ctx, fulltext);
TextLine[] textlines = Lineize(ctx, fulltext, linebreak, flowPath, startOffsetX, startY, font.LineHeightMultiplier, keepLinebreakChar);
@@ -296,7 +306,7 @@ public TextLine[] Lineize(Context ctx, string text, EnumLinebreakBehavior linebr
{
string spc = (gotLinebreak || caretPos >= text.Length || !gotSpace ? "" : " ");
- double nextWidth = ctx.TextExtents(lineTextBldr + word + spc).Width;
+ double nextWidth = GetRenderedTextWidth(ctx, lineTextBldr + word + spc);
currentSection = GetCurrentFlowPathSection(flowPath, curY);
@@ -321,7 +331,7 @@ public TextLine[] Lineize(Context ctx, string text, EnumLinebreakBehavior linebr
while (word.Length > 0 && nextWidth >= usableWidth && tries-- > 0)
{
word = word.Substring(0, word.Length - 1);
- nextWidth = ctx.TextExtents(lineTextBldr + word + spc).Width;
+ nextWidth = GetRenderedTextWidth(ctx, lineTextBldr + word + spc);
caretPos--;
}
@@ -331,7 +341,7 @@ public TextLine[] Lineize(Context ctx, string text, EnumLinebreakBehavior linebr
}
string linetext = lineTextBldr.ToString();
- double withoutWidth = ctx.TextExtents(linetext).Width;
+ double withoutWidth = GetRenderedTextWidth(ctx, linetext);
lines.Add(new TextLine()
{
@@ -361,7 +371,7 @@ public TextLine[] Lineize(Context ctx, string text, EnumLinebreakBehavior linebr
{
if (keepLinebreakChar) lineTextBldr.Append("\n");
string linetext = lineTextBldr.ToString();
- double withoutWidth = ctx.TextExtents(linetext).Width;
+ double withoutWidth = GetRenderedTextWidth(ctx, linetext);
lines.Add(new TextLine()
{
@@ -384,7 +394,7 @@ public TextLine[] Lineize(Context ctx, string text, EnumLinebreakBehavior linebr
if (currentSection == null) currentSection = new TextFlowPath();
usableWidth = currentSection.X2 - currentSection.X1 - curX;
string lineTextStr = lineTextBldr.ToString();
- double endWidth = ctx.TextExtents(lineTextStr).Width;
+ double endWidth = GetRenderedTextWidth(ctx, lineTextStr);
lines.Add(new TextLine()
{
@@ -486,11 +496,9 @@ public double AutobreakAndDrawMultilineText(Context ctx, CairoFont font, string
/// The preformatted lines of the text.
/// The font of the text
/// The orientation of text (Default: Left)
- public void DrawMultilineText(Context ctx, CairoFont font, TextLine[] lines, EnumTextOrientation orientation = EnumTextOrientation.Left)
- {
- font.SetupContext(ctx);
-
- double offsetX = 0;
+ public void DrawMultilineText(Context ctx, CairoFont font, TextLine[] lines, EnumTextOrientation orientation = EnumTextOrientation.Left)
+ {
+ double offsetX = 0;
for (int i = 0; i < lines.Length; i++)
{
@@ -520,38 +528,73 @@ public void DrawMultilineText(Context ctx, CairoFont font, TextLine[] lines, Enu
/// The X offset for the text start position. (Default: 0)
/// The Y offset for the text start position. (Default: 0)
/// Whether or not to use TextPathMode.
- public void DrawTextLine(Context ctx, CairoFont font, string text, double offsetX = 0, double offsetY = 0, bool textPathMode = false)
- {
- if (text == null || text.Length == 0) return;
-
- ctx.MoveTo((int)(offsetX), (int)(offsetY + ctx.FontExtents.Ascent));
-
- if (textPathMode)
- {
- ctx.TextPath(text);
- }
- else
- {
- if (font.StrokeWidth > 0)
- {
- ctx.TextPath(text);
- ctx.LineWidth = font.StrokeWidth;
- ctx.SetSourceRGBA(font.StrokeColor);
- ctx.StrokePreserve();
+ public void DrawTextLine(Context ctx, CairoFont font, string text, double offsetX = 0, double offsetY = 0, bool textPathMode = false)
+ {
+ font.SetupContext(ctx, text);
+ string renderedText = ComplexTextLayout.PrepareForRendering(text);
+ if (renderedText == null || renderedText.Length == 0) return;
+
+ double baselineY = offsetY + ctx.FontExtents.Ascent;
+
+ if (CairoGlyphLayout.TryCreateGlyphs(ctx, text, offsetX, baselineY, out Glyph[] glyphs))
+ {
+ if (textPathMode)
+ {
+ ctx.GlyphPath(glyphs);
+ return;
+ }
+
+ if (font.StrokeWidth > 0)
+ {
+ ctx.GlyphPath(glyphs);
+ ctx.LineWidth = font.StrokeWidth;
+ ctx.SetSourceRGBA(font.StrokeColor);
+ ctx.StrokePreserve();
+
+ ctx.SetSourceRGBA(font.Color);
+ ctx.Fill();
+ }
+ else
+ {
+ ctx.ShowGlyphs(glyphs);
+
+ if (font.RenderTwice)
+ {
+ ctx.ShowGlyphs(glyphs);
+ }
+ }
+
+ return;
+ }
+
+ ctx.MoveTo((int)(offsetX), (int)baselineY);
+
+ if (textPathMode)
+ {
+ ctx.TextPath(renderedText);
+ }
+ else
+ {
+ if (font.StrokeWidth > 0)
+ {
+ ctx.TextPath(renderedText);
+ ctx.LineWidth = font.StrokeWidth;
+ ctx.SetSourceRGBA(font.StrokeColor);
+ ctx.StrokePreserve();
ctx.SetSourceRGBA(font.Color);
ctx.Fill();
- }
- else
- {
- ctx.ShowText(text);
-
- if (font.RenderTwice)
- {
- ctx.ShowText(text);
- }
-
- }
+ }
+ else
+ {
+ ctx.ShowText(renderedText);
+
+ if (font.RenderTwice)
+ {
+ ctx.ShowText(renderedText);
+ }
+
+ }
}
}
#endregion
diff --git a/Client/UI/TextTextureUtil.cs b/Client/UI/TextTextureUtil.cs
index 75c45d44..01c79c49 100644
--- a/Client/UI/TextTextureUtil.cs
+++ b/Client/UI/TextTextureUtil.cs
@@ -132,41 +132,68 @@ public LoadedTexture GenTextTexture(string text, CairoFont font, int width, int
ctx.Stroke();
}
-
-
- font.SetupContext(ctx);
-
- double fontHeight = font.GetFontExtents().Height;
-
- string[] lines = text.Split('\n');
- for (int i = 0; i < lines.Length; i++)
- {
- lines[i] = lines[i].TrimEnd();
-
- ctx.MoveTo(background.HorPadding, background.VerPadding + ctx.FontExtents.Ascent + i * fontHeight);
-
- if (font.StrokeWidth > 0)
- {
- ctx.TextPath(lines[i]);
-
- ctx.LineWidth = font.StrokeWidth;
- ctx.SetSourceRGBA(font.StrokeColor);
+ font.SetupContext(ctx, text);
+
+ double fontHeight = font.GetFontExtents(text).Height;
+
+ string[] lines = text.Split('\n');
+ for (int i = 0; i < lines.Length; i++)
+ {
+ lines[i] = lines[i].TrimEnd();
+ font.SetupContext(ctx, lines[i]);
+ string renderedLine = ComplexTextLayout.PrepareForRendering(lines[i]);
+ double baselineY = background.VerPadding + ctx.FontExtents.Ascent + i * fontHeight;
+
+ if (CairoGlyphLayout.TryCreateGlyphs(ctx, lines[i], background.HorPadding, baselineY, out Glyph[] glyphs))
+ {
+ if (font.StrokeWidth > 0)
+ {
+ ctx.GlyphPath(glyphs);
+
+ ctx.LineWidth = font.StrokeWidth;
+ ctx.SetSourceRGBA(font.StrokeColor);
+ ctx.StrokePreserve();
+
+ ctx.SetSourceRGBA(font.Color);
+ ctx.Fill();
+ }
+ else
+ {
+ ctx.ShowGlyphs(glyphs);
+
+ if (font.RenderTwice)
+ {
+ ctx.ShowGlyphs(glyphs);
+ }
+ }
+
+ continue;
+ }
+
+ ctx.MoveTo(background.HorPadding, baselineY);
+
+ if (font.StrokeWidth > 0)
+ {
+ ctx.TextPath(renderedLine);
+
+ ctx.LineWidth = font.StrokeWidth;
+ ctx.SetSourceRGBA(font.StrokeColor);
ctx.StrokePreserve();
ctx.SetSourceRGBA(font.Color);
ctx.Fill();
- }
- else
- {
-
- ctx.ShowText(lines[i]);
-
- if (font.RenderTwice)
- {
- ctx.ShowText(lines[i]);
- }
-
- }
+ }
+ else
+ {
+
+ ctx.ShowText(renderedLine);
+
+ if (font.RenderTwice)
+ {
+ ctx.ShowText(renderedLine);
+ }
+
+ }
}
int textureId = capi.Gui.LoadCairoTexture(surface, true);