1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
package font
import "core:math"
// ============================================================================
// TEXT LAYOUT
// ============================================================================
Horizontal_Align :: enum {
Left,
Center,
Right,
}
Wrap_Mode :: enum {
None, // No wrapping
Word, // Break at word boundaries
Char, // Break at character boundaries
}
Layout_Settings :: struct {
x: f32, // starting x position
y: f32, // starting y position
scale: f32, // font scale (from scale_for_pixel_height)
line_height: f32, // line height multiplier (default 1.0)
max_width: f32, // max width before wrapping (0 = no limit)
h_align: Horizontal_Align,
wrap: Wrap_Mode,
apply_gsub: bool, // apply default GSUB substitutions
}
// Positioned glyph ready for rendering
Glyph_Position :: struct {
glyph_index: i32,
codepoint: i32,
x: f32, // baseline x position (subpixel precise)
y: f32, // baseline y position (subpixel precise)
x_advance: f32, // advance width (scaled)
x_offset: f32, // subpixel x offset (fractional part of x, for subpixel rendering)
y_offset: f32, // subpixel y offset
}
// Layout result
Layout_Result :: struct {
glyphs: []Glyph_Position,
line_count: i32,
total_height: f32,
}
// Lay out a UTF-8 string and return positioned glyphs.
// Caller must delete the returned glyphs slice.
layout_text :: proc(
info: ^Font_Info,
text: string,
settings: Layout_Settings,
allocator := context.allocator,
) -> Layout_Result {
if len(text) == 0 {
return {}
}
scale := settings.scale
if scale == 0 do return {}
// Get vertical metrics
ascent, descent, line_gap: i32
get_font_vmetrics(info, &ascent, &descent, &line_gap)
line_height_base := f32(ascent - descent + line_gap) * scale
line_height := line_height_base * (settings.line_height if settings.line_height > 0 else 1.0)
baseline_offset := f32(ascent) * scale
// First pass: convert codepoints to glyph indices
codepoints: [1024]i32
glyph_ids: [1024]i32
n: i32 = 0
i := 0
for i < len(text) && n < 1024 {
cp, size := decode_utf8(text[i:])
if size == 0 { i += 1; continue }
codepoints[n] = cp
glyph_ids[n] = find_glyph_index(info, cp)
n += 1
i += size
}
// Apply GSUB if requested
glyph_count := n
if settings.apply_gsub && info.gsub != 0 {
glyph_count = apply_gsub_default(info, glyph_ids[:n])
}
// Allocate output
positions, _ := make([]Glyph_Position, int(glyph_count), allocator)
// Second pass: position glyphs
cursor_x := settings.x
cursor_y := settings.y + baseline_offset
line_start: i32 = 0
line_num: i32 = 0
last_word_break: i32 = -1
for gi in 0..<glyph_count {
cp := codepoints[gi] if gi < n else 0
glyph := glyph_ids[gi]
// Handle newlines
if cp == '\n' {
// Align current line before starting new one
if settings.h_align != .Left && settings.max_width > 0 {
align_line(positions[line_start:gi], settings.max_width, settings.x, settings.h_align)
}
cursor_x = settings.x
cursor_y += line_height
line_start = gi + 1
line_num += 1
positions[gi] = {glyph_index = 0, codepoint = cp, x = cursor_x, y = cursor_y, x_advance = 0}
continue
}
// Track word boundaries for wrapping
if cp == ' ' || cp == '\t' {
last_word_break = gi
}
// Get horizontal metrics
adv_w, lsb: i32
get_glyph_hmetrics(info, glyph, &adv_w, &lsb)
x_advance := f32(adv_w) * scale
// Kerning with previous glyph
kern: f32 = 0
if gi > 0 {
kern = f32(get_glyph_kern_advance(info, glyph_ids[gi - 1], glyph)) * scale
cursor_x += kern
}
// Word wrap check
if settings.max_width > 0 && settings.wrap != .None {
if cursor_x + x_advance - settings.x > settings.max_width && gi > line_start {
// Align current line
if settings.h_align != .Left {
end := gi
if settings.wrap == .Word && last_word_break > line_start {
end = last_word_break + 1
}
align_line(positions[line_start:end], settings.max_width, settings.x, settings.h_align)
}
if settings.wrap == .Word && last_word_break > line_start {
// Reflow from word break
wrap_at := last_word_break + 1
cursor_x = settings.x
cursor_y += line_height
line_num += 1
// Reposition glyphs after the break
for ri in wrap_at..<gi {
adv_ri, lsb_ri: i32
get_glyph_hmetrics(info, glyph_ids[ri], &adv_ri, &lsb_ri)
positions[ri].x = cursor_x
positions[ri].y = cursor_y
cursor_x += f32(adv_ri) * scale
}
line_start = wrap_at
} else {
cursor_x = settings.x
cursor_y += line_height
line_start = gi
line_num += 1
}
last_word_break = -1
}
}
positions[gi] = {
glyph_index = glyph,
codepoint = cp,
x = cursor_x,
y = cursor_y,
x_advance = x_advance,
x_offset = cursor_x - math.floor(cursor_x),
y_offset = cursor_y - math.floor(cursor_y),
}
cursor_x += x_advance
}
// Align final line
if settings.h_align != .Left && settings.max_width > 0 {
align_line(positions[line_start:glyph_count], settings.max_width, settings.x, settings.h_align)
}
return {
glyphs = positions[:glyph_count],
line_count = line_num + 1,
total_height = f32(line_num + 1) * line_height,
}
}
@(private)
align_line :: proc(glyphs: []Glyph_Position, max_width: f32, origin_x: f32, align: Horizontal_Align) {
if len(glyphs) == 0 do return
// Find line width
last := glyphs[len(glyphs) - 1]
line_width := last.x + last.x_advance - origin_x
shift: f32
switch align {
case .Left: return
case .Center: shift = (max_width - line_width) / 2
case .Right: shift = max_width - line_width
}
for &g in glyphs {
g.x += shift
}
}
// Decode a single UTF-8 codepoint. Returns (codepoint, byte_count).
@(private)
decode_utf8 :: proc(s: string) -> (i32, int) {
if len(s) == 0 do return 0, 0
b0 := s[0]
if b0 < 0x80 {
return i32(b0), 1
}
if b0 < 0xC0 do return 0xFFFD, 1 // continuation byte
if b0 < 0xE0 {
if len(s) < 2 do return 0xFFFD, 1
return (i32(b0 & 0x1F) << 6) | i32(s[1] & 0x3F), 2
}
if b0 < 0xF0 {
if len(s) < 3 do return 0xFFFD, 1
return (i32(b0 & 0x0F) << 12) | (i32(s[1] & 0x3F) << 6) | i32(s[2] & 0x3F), 3
}
if b0 < 0xF8 {
if len(s) < 4 do return 0xFFFD, 1
return (i32(b0 & 0x07) << 18) | (i32(s[1] & 0x3F) << 12) | (i32(s[2] & 0x3F) << 6) | i32(s[3] & 0x3F), 4
}
return 0xFFFD, 1
}