Skip to content

Commit 4e36008

Browse files
Merge pull request #8 from TheEightBot/copilot/fix-japanese-text-garbled-android
2 parents cd9a44e + 080f5b1 commit 4e36008

9 files changed

Lines changed: 518 additions & 3 deletions

File tree

samples/SampleApp.Maui/AppShell.xaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@
3636
Icon="icon_actions.png"
3737
ContentTemplate="{DataTemplate local:MvvmActionsPage}"
3838
Route="MvvmActionsPage" />
39+
40+
<ShellContent
41+
Title="Custom Fonts"
42+
Icon="icon_font.png"
43+
ContentTemplate="{DataTemplate local:CustomFontsPage}"
44+
Route="CustomFontsPage" />
3945
</TabBar>
4046

4147
</Shell>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
4+
xmlns:dg="clr-namespace:KumikoUI.Maui;assembly=KumikoUI.Maui"
5+
xmlns:core="clr-namespace:KumikoUI.Core.Models;assembly=KumikoUI.Core"
6+
xmlns:render="clr-namespace:KumikoUI.Core.Rendering;assembly=KumikoUI.Core"
7+
x:Class="SampleApp.Maui.CustomFontsPage"
8+
Title="Custom Fonts Demo">
9+
10+
<Grid RowDefinitions="Auto,Auto,*,Auto,*" Padding="0">
11+
12+
<!-- ── Japanese headers section ───────────────────────────────── -->
13+
<Label Text="Japanese Column Headers (NotoSansJP font registered at startup)"
14+
FontSize="16"
15+
FontAttributes="Bold"
16+
Padding="16,12,16,4"
17+
Grid.Row="0" />
18+
19+
<Label Text="Headers: Hinpyo-ID / RFID / Hinban / Suryo — drawn from the NotoSansJP typeface via SkiaFontRegistrar."
20+
FontSize="13"
21+
TextColor="Gray"
22+
Padding="16,0,16,6"
23+
Grid.Row="1" />
24+
25+
<!-- Grid with Japanese headers — uses NotoSansJP via DataGridStyle.HeaderFont -->
26+
<dg:DataGridView x:Name="japaneseGrid"
27+
Grid.Row="2"
28+
RowHeight="36"
29+
HeaderHeight="44"
30+
EditTriggers="DoubleTap,F2Key"
31+
HorizontalOptions="Fill"
32+
VerticalOptions="Fill" />
33+
34+
<!-- ── Icon font section ───────────────────────────────────────── -->
35+
<Label Text="Icon Font Column (MaterialIcons)"
36+
FontSize="16"
37+
FontAttributes="Bold"
38+
Padding="16,12,16,4"
39+
Grid.Row="3" />
40+
41+
<!-- Grid with a status column rendered as Material Design icon glyphs -->
42+
<dg:DataGridView x:Name="iconGrid"
43+
Grid.Row="4"
44+
RowHeight="40"
45+
HeaderHeight="44"
46+
EditTriggers="None"
47+
HorizontalOptions="Fill"
48+
VerticalOptions="Fill" />
49+
50+
</Grid>
51+
52+
</ContentPage>
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
using System.Collections.ObjectModel;
2+
using System.ComponentModel;
3+
using System.Runtime.CompilerServices;
4+
using KumikoUI.Core.Models;
5+
using KumikoUI.Core.Rendering;
6+
7+
namespace SampleApp.Maui;
8+
9+
/// <summary>
10+
/// Demonstrates custom font support in KumikoUI:
11+
/// <list type="bullet">
12+
/// <item>Japanese (CJK) column headers using the NotoSansJP typeface registered at startup.</item>
13+
/// <item>Material Design icon glyphs in a status column using the MaterialIcons typeface.</item>
14+
/// </list>
15+
/// </summary>
16+
public partial class CustomFontsPage : ContentPage
17+
{
18+
// ── Material Icons Unicode code points ────────────────────────
19+
// These are the glyph addresses in the MaterialIcons-Regular.ttf font.
20+
private const string IconCheck = "\uE876"; // check
21+
private const string IconClose = "\uE5CD"; // close
22+
private const string IconWarning = "\uE002"; // warning (filled)
23+
private const string IconStar = "\uE838"; // star
24+
private const string IconPerson = "\uE7FD"; // person
25+
26+
public CustomFontsPage()
27+
{
28+
InitializeComponent();
29+
BuildJapaneseGrid();
30+
BuildIconGrid();
31+
}
32+
33+
// ── Japanese headers demo ────────────────────────────────────
34+
35+
private void BuildJapaneseGrid()
36+
{
37+
// Apply NotoSansJP for both header and cell text so Japanese characters render correctly.
38+
var style = new DataGridStyle
39+
{
40+
HeaderFont = new GridFont("NotoSansJP", 14, bold: true),
41+
CellFont = new GridFont("NotoSansJP", 13),
42+
};
43+
44+
japaneseGrid.GridStyle = style;
45+
japaneseGrid.Columns.Add(new DataGridColumn
46+
{
47+
Header = "現品票ID",
48+
PropertyName = nameof(InventoryItem.ItemId),
49+
Width = 110,
50+
ColumnType = DataGridColumnType.Numeric,
51+
TextAlignment = GridTextAlignment.Right,
52+
IsReadOnly = true,
53+
IsFrozen = true,
54+
});
55+
japaneseGrid.Columns.Add(new DataGridColumn
56+
{
57+
Header = "RFID",
58+
PropertyName = nameof(InventoryItem.Rfid),
59+
Width = 160,
60+
});
61+
japaneseGrid.Columns.Add(new DataGridColumn
62+
{
63+
Header = "品番",
64+
PropertyName = nameof(InventoryItem.PartNumber),
65+
Width = 130,
66+
});
67+
japaneseGrid.Columns.Add(new DataGridColumn
68+
{
69+
Header = "数量",
70+
PropertyName = nameof(InventoryItem.Quantity),
71+
Width = 90,
72+
ColumnType = DataGridColumnType.Numeric,
73+
TextAlignment = GridTextAlignment.Right,
74+
});
75+
japaneseGrid.Columns.Add(new DataGridColumn
76+
{
77+
Header = "倉庫",
78+
PropertyName = nameof(InventoryItem.Warehouse),
79+
Width = 120,
80+
});
81+
82+
japaneseGrid.ItemsSource = GenerateInventoryData();
83+
}
84+
85+
private static ObservableCollection<InventoryItem> GenerateInventoryData()
86+
{
87+
string[] rfidPrefixes = ["JP-TK-", "JP-OS-", "JP-KY-"];
88+
string[] partNumbers = ["A-001-α", "B-002-β", "C-003-γ", "D-004-δ", "E-005-ε"];
89+
string[] warehouses = ["東京", "大阪", "京都", "名古屋", "福岡"];
90+
var rng = new Random(1);
91+
var items = new ObservableCollection<InventoryItem>();
92+
for (int i = 1; i <= 60; i++)
93+
{
94+
items.Add(new InventoryItem
95+
{
96+
ItemId = 1000 + i,
97+
Rfid = $"{rfidPrefixes[rng.Next(rfidPrefixes.Length)]}{i:D4}",
98+
PartNumber = partNumbers[rng.Next(partNumbers.Length)],
99+
Quantity = rng.Next(1, 500),
100+
Warehouse = warehouses[rng.Next(warehouses.Length)],
101+
});
102+
}
103+
return items;
104+
}
105+
106+
// ── Icon font demo ───────────────────────────────────────────
107+
108+
private void BuildIconGrid()
109+
{
110+
// Regular columns use the default font; only the Status column uses MaterialIcons.
111+
iconGrid.Columns.Add(new DataGridColumn
112+
{
113+
Header = "Name",
114+
PropertyName = nameof(TaskItem.Name),
115+
Width = 180,
116+
IsFrozen = true,
117+
});
118+
iconGrid.Columns.Add(new DataGridColumn
119+
{
120+
Header = "Assignee",
121+
PropertyName = nameof(TaskItem.Assignee),
122+
Width = 130,
123+
});
124+
iconGrid.Columns.Add(new DataGridColumn
125+
{
126+
Header = "Priority",
127+
PropertyName = nameof(TaskItem.PriorityIcon),
128+
Width = 80,
129+
IsReadOnly = true,
130+
TextAlignment = GridTextAlignment.Center,
131+
// Override the font for this column's cells so glyphs are drawn
132+
// from the MaterialIcons typeface registered at startup.
133+
CustomCellRenderer = new IconFontCellRenderer("MaterialIcons", 22f),
134+
});
135+
iconGrid.Columns.Add(new DataGridColumn
136+
{
137+
Header = "Done",
138+
PropertyName = nameof(TaskItem.DoneIcon),
139+
Width = 60,
140+
IsReadOnly = true,
141+
TextAlignment = GridTextAlignment.Center,
142+
CustomCellRenderer = new IconFontCellRenderer("MaterialIcons", 22f),
143+
});
144+
145+
iconGrid.ItemsSource = GenerateTaskData();
146+
}
147+
148+
private ObservableCollection<TaskItem> GenerateTaskData()
149+
{
150+
string[] names = ["Implement login", "Fix crash on startup", "Write unit tests",
151+
"Add dark theme", "Optimize SQL queries", "Update documentation",
152+
"Code review PR #42", "Deploy to staging", "Load testing",
153+
"Security audit"];
154+
string[] assignees = ["Alice", "Bob", "Charlie", "Diana", "Eve",
155+
"Frank", "Grace", "Hank", "Iris", "Jack"];
156+
var rng = new Random(2);
157+
var items = new ObservableCollection<TaskItem>();
158+
for (int i = 0; i < names.Length; i++)
159+
{
160+
var priority = rng.Next(3);
161+
var done = rng.NextDouble() > 0.5;
162+
items.Add(new TaskItem
163+
{
164+
Name = names[i],
165+
Assignee = assignees[i % assignees.Length],
166+
PriorityIcon = priority switch { 0 => IconWarning, 1 => IconStar, _ => "" },
167+
DoneIcon = done ? IconCheck : IconClose,
168+
IsDone = done,
169+
Priority = priority,
170+
});
171+
}
172+
return items;
173+
}
174+
}
175+
176+
// ── Data models ──────────────────────────────────────────────────
177+
178+
/// <summary>Inventory item with Japanese property-value data.</summary>
179+
public class InventoryItem : INotifyPropertyChanged
180+
{
181+
private int _itemId;
182+
private string _rfid = string.Empty;
183+
private string _partNumber = string.Empty;
184+
private int _quantity;
185+
private string _warehouse = string.Empty;
186+
187+
public int ItemId { get => _itemId; set { _itemId = value; OnPropertyChanged(); } }
188+
public string Rfid { get => _rfid; set { _rfid = value; OnPropertyChanged(); } }
189+
public string PartNumber { get => _partNumber; set { _partNumber = value; OnPropertyChanged(); } }
190+
public int Quantity { get => _quantity; set { _quantity = value; OnPropertyChanged(); } }
191+
public string Warehouse { get => _warehouse; set { _warehouse = value; OnPropertyChanged(); } }
192+
193+
public event PropertyChangedEventHandler? PropertyChanged;
194+
protected void OnPropertyChanged([CallerMemberName] string? name = null) =>
195+
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
196+
}
197+
198+
/// <summary>Task item whose Priority and Done columns are rendered as Material Icons glyphs.</summary>
199+
public class TaskItem : INotifyPropertyChanged
200+
{
201+
private string _name = string.Empty;
202+
private string _assignee = string.Empty;
203+
private string _priorityIcon = string.Empty;
204+
private string _doneIcon = string.Empty;
205+
private bool _isDone;
206+
private int _priority;
207+
208+
public string Name { get => _name; set { _name = value; OnPropertyChanged(); } }
209+
public string Assignee { get => _assignee; set { _assignee = value; OnPropertyChanged(); } }
210+
public string PriorityIcon { get => _priorityIcon; set { _priorityIcon = value; OnPropertyChanged(); } }
211+
public string DoneIcon { get => _doneIcon; set { _doneIcon = value; OnPropertyChanged(); } }
212+
public bool IsDone { get => _isDone; set { _isDone = value; OnPropertyChanged(); } }
213+
public int Priority { get => _priority; set { _priority = value; OnPropertyChanged(); } }
214+
215+
public event PropertyChangedEventHandler? PropertyChanged;
216+
protected void OnPropertyChanged([CallerMemberName] string? name = null) =>
217+
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
218+
}
219+
220+
// ── Icon font renderer ───────────────────────────────────────────
221+
222+
/// <summary>
223+
/// A simple <see cref="ICellRenderer"/> that draws cell text using a named icon font.
224+
/// Set the cell value to a Unicode code-point string (e.g. <c>"\uE876"</c>) and the renderer
225+
/// will draw the corresponding glyph from the registered typeface.
226+
/// </summary>
227+
internal sealed class IconFontCellRenderer : ICellRenderer
228+
{
229+
private readonly string _fontFamily;
230+
private readonly float _fontSize;
231+
232+
/// <param name="fontFamily">
233+
/// Family name registered in <see cref="KumikoUI.SkiaSharp.SkiaFontRegistrar"/>.
234+
/// </param>
235+
/// <param name="fontSize">Glyph size in pixels.</param>
236+
public IconFontCellRenderer(string fontFamily, float fontSize)
237+
{
238+
_fontFamily = fontFamily;
239+
_fontSize = fontSize;
240+
}
241+
242+
/// <inheritdoc />
243+
public void Render(
244+
IDrawingContext ctx,
245+
GridRect cellRect,
246+
object? value,
247+
string displayText,
248+
DataGridColumn column,
249+
DataGridStyle style,
250+
bool isSelected,
251+
CellStyle? cellStyle = null)
252+
{
253+
if (string.IsNullOrEmpty(displayText))
254+
return;
255+
256+
var textColor = isSelected ? style.SelectionTextColor : style.CellTextColor;
257+
var paint = new GridPaint
258+
{
259+
Color = textColor,
260+
IsAntiAlias = true,
261+
Font = new GridFont(_fontFamily, _fontSize),
262+
};
263+
264+
ctx.DrawTextInRect(displayText, cellRect, paint,
265+
GridTextAlignment.Center, GridVerticalAlignment.Center);
266+
}
267+
}

samples/SampleApp.Maui/MauiProgram.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using KumikoUI.Maui;
2+
using KumikoUI.SkiaSharp;
23
using Microsoft.Extensions.Logging;
34

45
namespace SampleApp.Maui;
@@ -17,10 +18,52 @@ public static MauiApp CreateMauiApp()
1718
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
1819
});
1920

21+
// Register custom SkiaSharp typefaces for use in the grid renderer.
22+
// Fonts placed in Resources/Raw/ can be loaded here and made available
23+
// to DataGridStyle via GridFont.Family. This is the recommended fix for
24+
// garbled CJK text and icon-font glyphs on Android, where the system font
25+
// manager does not expose Japanese or icon-font typefaces.
26+
RegisterSkiaFonts();
27+
2028
#if DEBUG
2129
builder.Logging.AddDebug();
2230
#endif
2331

2432
return builder.Build();
2533
}
34+
35+
/// <summary>
36+
/// Loads custom font files from the app bundle and registers them with
37+
/// <see cref="SkiaFontRegistrar"/> so <see cref="KumikoUI.SkiaSharp.SkiaDrawingContext"/>
38+
/// can resolve them by family name.
39+
/// </summary>
40+
private static void RegisterSkiaFonts()
41+
{
42+
// Noto Sans JP — covers Japanese, Chinese and Korean characters.
43+
// Family name "NotoSansJP" can be used in DataGridStyle.HeaderFont / CellFont.
44+
TryRegisterFont("NotoSansJP", "NotoSansJP-Regular.ttf");
45+
46+
// Material Icons — icon glyph font. Use the Unicode code points (e.g. "\uE88A")
47+
// as cell values and set the column font family to "MaterialIcons".
48+
TryRegisterFont("MaterialIcons", "MaterialIcons-Regular.ttf");
49+
}
50+
51+
private static void TryRegisterFont(string family, string assetFileName)
52+
{
53+
try
54+
{
55+
// Blocking on async file I/O is safe here: MauiProgram.CreateMauiApp() runs
56+
// before the MAUI UI message-loop starts, so there is no running
57+
// synchronization context that could deadlock on `.GetAwaiter().GetResult()`.
58+
using var stream = FileSystem.OpenAppPackageFileAsync(assetFileName)
59+
.GetAwaiter().GetResult();
60+
SkiaFontRegistrar.RegisterTypefaceFromStream(family, stream);
61+
}
62+
catch (Exception ex)
63+
{
64+
// Non-fatal — the grid falls back to the system default font.
65+
System.Diagnostics.Debug.WriteLine(
66+
$"[KumikoUI] Could not register font '{family}' from '{assetFileName}': {ex.Message}");
67+
}
68+
}
2669
}
Lines changed: 3 additions & 0 deletions
Loading
348 KB
Binary file not shown.
9.15 MB
Binary file not shown.

0 commit comments

Comments
 (0)