Skip to content

Commit 1fdce7d

Browse files
committed
Allow specifying language specific fonts
1 parent 5f402a4 commit 1fdce7d

3 files changed

Lines changed: 179 additions & 64 deletions

File tree

src/chunker.zig

Lines changed: 119 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ pub const Chunker = struct {
77
};
88
}
99

10-
pub fn next(self: *Chunker) ?[]const u8 {
10+
pub fn next(self: *Chunker, display: *Display) ?TextElement {
1111
if (self.data.len == 0) {
1212
return null;
1313
}
@@ -24,7 +24,7 @@ pub const Chunker = struct {
2424
}
2525
self.data.ptr += 1;
2626
self.data.len -= 1;
27-
return cr;
27+
return .{ .text = cr, .width = 0, .font = display.font.default, .texture = undefined };
2828
}
2929
if (!is_whitespace(self.data[0])) {
3030
break;
@@ -35,24 +35,29 @@ pub const Chunker = struct {
3535

3636
var end: usize = 0;
3737
while (self.data.len > end) {
38-
if (self.data[end] == '\\' and self.data.len > end + 1 and self.data[end + 1] == 'n') {
38+
const c = self.data[end];
39+
if (c == '\\' and self.data.len > end + 1 and self.data[end + 1] == 'n') {
3940
if (end == 0) {
4041
self.data.ptr += 2;
4142
self.data.len -= 2;
42-
return cr;
43+
return .{ .text = cr, .width = 0, .font = display.font.default, .texture = undefined };
4344
}
4445
break;
4546
}
46-
if (is_whitespace_or_eol(self.data[end])) {
47-
break;
48-
}
47+
if (is_whitespace_or_eol(c)) break;
4948
end += 1;
5049
}
5150

5251
const token = self.data[0..end];
5352
self.data.ptr += end;
5453
self.data.len -= end;
55-
return token;
54+
55+
return .{
56+
.text = token,
57+
.font = guess_language(token, display),
58+
.width = 0,
59+
.texture = undefined,
60+
};
5661
}
5762
};
5863

@@ -70,73 +75,144 @@ pub inline fn is_eol(c: u8) bool {
7075
return c == '\n' or c == '\r' or c == 0;
7176
}
7277

73-
const eql = @import("std").mem.eql;
74-
const std = @import("std");
75-
const unicode = @import("std").unicode;
76-
const expect = std.testing.expect;
77-
const expectEqualDeep = std.testing.expectEqualDeep;
78-
const expectEqual = std.testing.expectEqual;
79-
const expectEqualStrings = std.testing.expectEqualStrings;
78+
pub fn guess_language(word: []const u8, display: *Display) *Font {
79+
var lang = Lang.unknown;
80+
var v = Utf8View.init(word) catch {
81+
return display.font.default;
82+
};
83+
var it = v.iterator();
84+
while (it.nextCodepoint()) |c| {
85+
// Grouping characters are not meaningful for detecting language
86+
if (c == '{' or c == '}' or c == '[' or c == ']') continue;
87+
88+
var x = Lang.unknown;
89+
if (is_greek_letter(c)) {
90+
x = .greek;
91+
} else if (is_english_letter(c)) {
92+
x = .english;
93+
} else if (c == ' ' or c == '\n' or c == '\t' or c == '"' or c == '\'') {
94+
continue;
95+
} else if (is_greek_punctuation(c)) {
96+
continue;
97+
} else if (is_english_punctuation(c)) {
98+
if (lang == .greek) return display.font.default;
99+
continue;
100+
} else {
101+
return display.font.default;
102+
}
103+
if (lang == .unknown) {
104+
lang = x;
105+
continue;
106+
}
107+
if (lang != x) {
108+
warn("Mixed language. Detection failed. {s}", .{word});
109+
return display.font.default;
110+
}
111+
}
112+
//debug(" chunk {t} {s}", .{ lang, word });
113+
return switch (lang) {
114+
.english => display.font.english,
115+
.greek => display.font.greek,
116+
else => display.font.default,
117+
};
118+
}
119+
120+
pub inline fn is_english_punctuation(c: u21) bool {
121+
return (c == '.' or c == '?' or c == ',' or c == '"' or c == '\'' or c == '!' or c == '/' or c == '\u{2018}' or c == '\u{2019}' or c == '(' or c == ')' or c == ';' or c == '-');
122+
}
123+
124+
pub inline fn is_greek_punctuation(c: u21) bool {
125+
return (c == '.' or c == ';' or c == ',' or c == '"' or c == '!' or c == '·' or c == '«' or c == '»');
126+
}
127+
128+
pub inline fn is_english_letter(c: u21) bool {
129+
return (c >= 'A' and c <= 'Z') or (c >= 'a' and c <= 'z') or (c >= '0' and c <= '9');
130+
}
131+
132+
pub inline fn is_greek_letter(c: u21) bool {
133+
return (c >= '\u{0370}' and c <= '\u{03FF}') or (c >= '\u{1f00}' and c <= '\u{1ffF}');
134+
}
80135

81136
test "read_chunks" {
82137
var data = Chunker.init("the fish");
83-
try expectEqualStrings("the", data.next().?);
84-
try expectEqualStrings("fish", data.next().?);
138+
try expectEqualStrings("the", data.next().?.text);
139+
try expectEqualStrings("fish", data.next().?.text);
85140
try expectEqual(null, data.next());
86141

87142
data = Chunker.init("");
88143
try expectEqual(null, data.next());
89144

90145
data = Chunker.init("τίς βλέπει");
91-
try expectEqualStrings("τίς", data.next().?);
92-
try expectEqualStrings("βλέπει", data.next().?);
146+
try expectEqualStrings("τίς", data.next().?.text);
147+
try expectEqualStrings("βλέπει", data.next().?.text);
93148
try expectEqual(null, data.next());
94149

95150
data = Chunker.init("God, god.");
96-
try expectEqualStrings("God,", data.next().?);
97-
try expectEqualStrings("god.", data.next().?);
151+
try expectEqualStrings("God,", data.next().?.text);
152+
try expectEqualStrings("god.", data.next().?.text);
98153
try expectEqual(null, data.next());
99154

100155
data = Chunker.init("fish\ncat\n");
101-
try expectEqualStrings("fish", data.next().?);
102-
try expectEqualStrings("\n", data.next().?);
103-
try expectEqualStrings("cat", data.next().?);
104-
try expectEqualStrings("\n", data.next().?);
156+
try expectEqualStrings("fish", data.next().?.text);
157+
try expectEqualStrings("\n", data.next().?.text);
158+
try expectEqualStrings("cat", data.next().?.text);
159+
try expectEqualStrings("\n", data.next().?.text);
105160
try expectEqual(null, data.next());
106161

107162
data = Chunker.init(" 'fish' \n [cat] \n");
108-
try expectEqualStrings("'fish'", data.next().?);
109-
try expectEqualStrings("\n", data.next().?);
110-
try expectEqualStrings("[cat]", data.next().?);
111-
try expectEqualStrings("\n", data.next().?);
163+
try expectEqualStrings("'fish'", data.next().?.text);
164+
try expectEqualStrings("\n", data.next().?.text);
165+
try expectEqualStrings("[cat]", data.next().?.text);
166+
try expectEqualStrings("\n", data.next().?.text);
112167
try expectEqual(null, data.next());
113168

114169
data = Chunker.init("fish\n\ncat");
115-
try expectEqualStrings("fish", data.next().?);
116-
try expectEqualStrings("\n", data.next().?);
117-
try expectEqualStrings("\n", data.next().?);
118-
try expectEqualStrings("cat", data.next().?);
170+
try expectEqualStrings("fish", data.next().?.text);
171+
try expectEqualStrings("\n", data.next().?.text);
172+
try expectEqualStrings("\n", data.next().?.text);
173+
try expectEqualStrings("cat", data.next().?.text);
119174
try expectEqual(null, data.next());
120175

121176
data = Chunker.init("fish\r\n\n\rcat");
122-
try expectEqualStrings("fish", data.next().?);
123-
try expectEqualStrings("\n", data.next().?);
124-
try expectEqualStrings("\n", data.next().?);
125-
try expectEqualStrings("cat", data.next().?);
177+
try expectEqualStrings("fish", data.next().?.text);
178+
try expectEqualStrings("\n", data.next().?.text);
179+
try expectEqualStrings("\n", data.next().?.text);
180+
try expectEqualStrings("cat", data.next().?.text);
126181
try expectEqual(null, data.next());
127182

128183
data = Chunker.init("\\ncat");
129-
try expectEqualStrings("\n", data.next().?);
130-
try expectEqualStrings("cat", data.next().?);
184+
try expectEqualStrings("\n", data.next().?.text);
185+
try expectEqualStrings("cat", data.next().?.text);
131186
try expectEqual(null, data.next());
132187

133188
data = Chunker.init("fish\\cat");
134-
try expectEqualStrings("fish\\cat", data.next().?);
189+
try expectEqualStrings("fish\\cat", data.next().?.text);
135190
try expectEqual(null, data.next());
136191

137192
data = Chunker.init("fish\\ncat");
138-
try expectEqualStrings("fish", data.next().?);
139-
try expectEqualStrings("\n", data.next().?);
140-
try expectEqualStrings("cat", data.next().?);
193+
try expectEqualStrings("fish", data.next().?.text);
194+
try expectEqualStrings("\n", data.next().?.text);
195+
try expectEqualStrings("cat", data.next().?.text);
141196
try expectEqual(null, data.next());
142197
}
198+
199+
const eql = @import("std").mem.eql;
200+
const std = @import("std");
201+
const unicode = @import("std").unicode;
202+
const Utf8View = std.unicode.Utf8View;
203+
204+
const praxis = @import("praxis");
205+
const Lang = praxis.Lang;
206+
207+
const TextElement = @import("element.zig").TextElement;
208+
209+
const engine = @import("engine.zig");
210+
const Display = engine.Display;
211+
const Font = engine.Font;
212+
const debug = engine.debug;
213+
const warn = engine.warn;
214+
215+
const expect = std.testing.expect;
216+
const expectEqualDeep = std.testing.expectEqualDeep;
217+
const expectEqual = std.testing.expectEqual;
218+
const expectEqualStrings = std.testing.expectEqualStrings;

src/element.zig

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -704,14 +704,27 @@ pub const Element = struct {
704704
self.type.label.translated = new_translated;
705705
if (self.type.label.translated.len > 0) {
706706
var data = Chunker.init(self.type.label.translated);
707-
while (data.next()) |text| {
708-
if (display.generate_text_texture(text, self.type.label.font)) |texture| {
709-
try self.type.label.elements.append(allocator, .{
710-
.text = text,
711-
.width = @floatFromInt(texture.*.w),
712-
.texture = texture,
713-
.location = .{}, // Location of each element is unknown at this point
714-
});
707+
if (self.type.label.font_name) |_| {
708+
while (data.next(display)) |word| {
709+
if (display.generate_text_texture(word.text, self.type.label.font)) |texture| {
710+
try self.type.label.elements.append(allocator, .{
711+
.text = word.text,
712+
.width = @floatFromInt(texture.*.w),
713+
.texture = texture,
714+
.font = word.font,
715+
});
716+
}
717+
}
718+
} else {
719+
while (data.next(display)) |word| {
720+
if (display.generate_text_texture(word.text, word.font)) |texture| {
721+
try self.type.label.elements.append(allocator, .{
722+
.text = word.text,
723+
.width = @floatFromInt(texture.*.w),
724+
.texture = texture,
725+
.font = word.font,
726+
});
727+
}
715728
}
716729
}
717730
}
@@ -727,14 +740,27 @@ pub const Element = struct {
727740
if (self.type.checkbox.translated.len > 0) {
728741
self.type.checkbox.elements.clearRetainingCapacity();
729742
var data = Chunker.init(self.type.checkbox.translated);
730-
while (data.next()) |text| {
731-
if (display.generate_text_texture(text, self.type.checkbox.font)) |texture| {
732-
try self.type.checkbox.elements.append(allocator, .{
733-
.text = text,
734-
.width = @floatFromInt(texture.*.w),
735-
.texture = texture,
736-
.location = .{}, // Location of each element is unknown at this point
737-
});
743+
if (self.type.checkbox.font_name) |_| {
744+
while (data.next(display)) |text| {
745+
if (display.generate_text_texture(text.text, self.type.checkbox.font)) |texture| {
746+
try self.type.checkbox.elements.append(allocator, .{
747+
.text = text.text,
748+
.width = @floatFromInt(texture.*.w),
749+
.texture = texture,
750+
.font = text.font,
751+
});
752+
}
753+
}
754+
} else {
755+
while (data.next(display)) |text| {
756+
if (display.generate_text_texture(text.text, text.font)) |texture| {
757+
try self.type.checkbox.elements.append(allocator, .{
758+
.text = text.text,
759+
.width = @floatFromInt(texture.*.w),
760+
.texture = texture,
761+
.font = text.font,
762+
});
763+
}
738764
}
739765
}
740766
}
@@ -1542,7 +1568,7 @@ pub const Element = struct {
15421568
// Lay down each word one by one and wrap before we hit the
15431569
// `wrap_at` boundary.
15441570
for (children, 0..) |*item, i| {
1545-
const is_cr = item.text != null and item.text.?.len == 1 and item.text.?[0] == '\n';
1571+
const is_cr = item.text.len == 1 and item.text[0] == '\n';
15461572
const size = text_height.pixel_size(display, item.texture);
15471573
// Would drawing this word overflow?
15481574
if ((x + word_spacing + size.width > wrap_at and line_word_count > 0) or is_cr) {
@@ -2296,11 +2322,12 @@ pub const Background = struct {
22962322
corner_radius: f32 = 0,
22972323
};
22982324

2299-
const TextElement = struct {
2300-
text: ?[]const u8,
2301-
width: f32, // compared to default height
2325+
pub const TextElement = struct {
2326+
text: []const u8 = "",
2327+
font: *Font,
2328+
width: f32 = 0, // compared to default height
23022329
texture: *sdl.SDL_Texture,
2303-
location: Rect,
2330+
location: Rect = .{ .x = 0, .y = 0, .width = 0, .height = 0 },
23042331
};
23052332

23062333
/// A vector may represent a position or distance in 2D space.

src/engine.zig

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ pub const Display = struct {
4141
/// A list of all active fonts loaded from the resources bundle.
4242
fonts: ArrayListUnmanaged(*Font) = .empty,
4343

44+
font: struct {
45+
default: *Font,
46+
english: *Font,
47+
greek: *Font,
48+
},
49+
4450
/// Translates the default provided text into a specific language
4551
/// using a csv translation file
4652
translation: Translation,
@@ -1269,6 +1275,12 @@ pub const Display = struct {
12691275
errdefer font_info.destroy(allocator);
12701276
try self.fonts.append(allocator, font_info);
12711277

1278+
if (self.fonts.items.len == 1) {
1279+
self.font.default = font_info;
1280+
self.font.english = font_info;
1281+
self.font.greek = font_info;
1282+
}
1283+
12721284
if (self.fonts.items.len > 1) {
12731285
const i = self.fonts.items.len - 2;
12741286
_ = sdl.TTF_AddFallbackFont(self.fonts.items[i].font, self.fonts.items[i + 1].font);

0 commit comments

Comments
 (0)