v2 / vlib / gg / text_rendering.c.v
338 lines · 317 sloc · 9.29 KB · 60072360e1e01ac0134def097f606c528c4f8e49
Raw
1// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by an MIT license that can be found in the LICENSE file.
3module gg
4
5import fontstash
6import sokol.sfons
7import sokol.sgl
8import os
9import os.font
10
11pub struct FT {
12pub:
13 fons &fontstash.Context = unsafe { nil }
14 font_normal int
15 font_bold int
16 font_mono int
17 font_italic int
18pub mut:
19 fonts_map map[string]int // for storing custom fonts, provided via cfg.family in draw_text()
20 scale f32 = 1.0
21}
22
23pub enum HorizontalAlign {
24 left = C.FONS_ALIGN_LEFT
25 center = C.FONS_ALIGN_CENTER
26 right = C.FONS_ALIGN_RIGHT
27}
28
29pub enum VerticalAlign {
30 top = C.FONS_ALIGN_TOP
31 middle = C.FONS_ALIGN_MIDDLE
32 bottom = C.FONS_ALIGN_BOTTOM
33 baseline = C.FONS_ALIGN_BASELINE
34}
35
36const initial_text_atlas_size = int($d('gg_text_buff_size', 2048))
37const max_text_atlas_size = 8192
38
39fn expand_atlas_callback(uptr voidptr, error int, _val int) {
40 if error != C.FONS_ATLAS_FULL {
41 return
42 }
43 fons := unsafe { &fontstash.Context(uptr) }
44 width, height := fons.get_atlas_size()
45 mut next_width := if width > 0 { width } else { initial_text_atlas_size }
46 mut next_height := if height > 0 { height } else { initial_text_atlas_size }
47 if next_width < max_text_atlas_size {
48 next_width = if next_width * 2 > max_text_atlas_size {
49 max_text_atlas_size
50 } else {
51 next_width * 2
52 }
53 }
54 if next_height < max_text_atlas_size {
55 next_height = if next_height * 2 > max_text_atlas_size {
56 max_text_atlas_size
57 } else {
58 next_height * 2
59 }
60 }
61 if next_width == width && next_height == height {
62 return
63 }
64 fons.expand_atlas(next_width, next_height)
65}
66
67fn new_ft(c FTConfig) ?&FT {
68 if c.font_path == '' {
69 if c.bytes_normal.len > 0 {
70 fons := sfons.create(initial_text_atlas_size, initial_text_atlas_size, 1)
71 bytes_normal := c.bytes_normal
72 bytes_bold := if c.bytes_bold.len > 0 {
73 c.bytes_bold
74 } else {
75 debug_font_println('setting bold variant to normal')
76 bytes_normal
77 }
78 bytes_mono := if c.bytes_mono.len > 0 {
79 c.bytes_mono
80 } else {
81 debug_font_println('setting mono variant to normal')
82 bytes_normal
83 }
84 bytes_italic := if c.bytes_italic.len > 0 {
85 c.bytes_italic
86 } else {
87 debug_font_println('setting italic variant to normal')
88 bytes_normal
89 }
90 fons.set_error_callback(expand_atlas_callback, fons)
91 return &FT{
92 fons: fons
93 font_normal: fons.add_font_mem('sans', bytes_normal.clone(), true)
94 font_bold: fons.add_font_mem('sans', bytes_bold.clone(), true)
95 font_mono: fons.add_font_mem('sans', bytes_mono.clone(), true)
96 font_italic: fons.add_font_mem('sans', bytes_italic.clone(), true)
97 scale: c.scale
98 }
99 } else {
100 // Load default font
101 }
102 }
103
104 if c.font_path == '' || !os.exists(c.font_path) {
105 $if !android {
106 println('failed to load font "${c.font_path}"')
107 return none
108 }
109 }
110
111 mut normal_path := c.font_path
112 mut bytes := []u8{}
113 $if android {
114 // First try any filesystem paths
115 bytes = os.read_bytes(c.font_path) or { []u8{} }
116 if bytes.len == 0 {
117 // ... then try the APK asset path
118 bytes = os.read_apk_asset(c.font_path) or {
119 println('failed to load font "${c.font_path}"')
120 return none
121 }
122 }
123 } $else {
124 bytes = os.read_bytes(c.font_path) or {
125 println('failed to load font "${c.font_path}"')
126 return none
127 }
128 }
129 mut bold_path := if c.custom_bold_font_path != '' {
130 c.custom_bold_font_path
131 } else {
132 font.get_path_variant(c.font_path, .bold)
133 }
134 bytes_bold := os.read_bytes(bold_path) or {
135 debug_font_println('failed to load font "${bold_path}"')
136 bold_path = c.font_path
137 bytes
138 }
139 mut mono_path := font.get_path_variant(c.font_path, .mono)
140 bytes_mono := os.read_bytes(mono_path) or {
141 debug_font_println('failed to load font "${mono_path}"')
142 mono_path = c.font_path
143 bytes
144 }
145 mut italic_path := font.get_path_variant(c.font_path, .italic)
146 bytes_italic := os.read_bytes(italic_path) or {
147 debug_font_println('failed to load font "${italic_path}"')
148 italic_path = c.font_path
149 bytes
150 }
151 fons := sfons.create(initial_text_atlas_size, initial_text_atlas_size, 1)
152 debug_font_println('Font used for font_normal : ${normal_path}')
153 debug_font_println('Font used for font_bold : ${bold_path}')
154 debug_font_println('Font used for font_mono : ${mono_path}')
155 debug_font_println('Font used for font_italic : ${italic_path}')
156 fons.set_error_callback(expand_atlas_callback, fons)
157 return &FT{
158 fons: fons
159 font_normal: fons.add_font_mem('sans', bytes.clone(), true)
160 font_bold: fons.add_font_mem('sans', bytes_bold.clone(), true)
161 font_mono: fons.add_font_mem('sans', bytes_mono.clone(), true)
162 font_italic: fons.add_font_mem('sans', bytes_italic.clone(), true)
163 scale: c.scale
164 }
165}
166
167// set_text_cfg sets the current text configuration
168pub fn (ctx &Context) set_text_cfg(cfg TextCfg) {
169 if !ctx.font_inited {
170 return
171 }
172 if cfg.family != '' {
173 // println('set text cfg family=${cfg.family}')
174 mut f := ctx.ft.fonts_map[cfg.family]
175 if f == 0 {
176 // No such font in the cache yet, create it
177 bytes := os.read_bytes(cfg.family) or {
178 debug_font_println('failed to load font "${cfg.family}"')
179 return
180 }
181 f = ctx.ft.fons.add_font_mem(cfg.family, bytes.clone(), true)
182 unsafe {
183 ctx.ft.fonts_map[cfg.family] = f
184 }
185 }
186 ctx.ft.fons.set_font(f)
187 } else if cfg.bold {
188 ctx.ft.fons.set_font(ctx.ft.font_bold)
189 } else if cfg.mono {
190 ctx.ft.fons.set_font(ctx.ft.font_mono)
191 } else if cfg.italic {
192 ctx.ft.fons.set_font(ctx.ft.font_italic)
193 } else {
194 ctx.ft.fons.set_font(ctx.ft.font_normal)
195 }
196 scale := if ctx.ft.scale == 0 { f32(1) } else { ctx.ft.scale }
197 size := if cfg.mono { cfg.size - 2 } else { cfg.size }
198 ctx.ft.fons.set_size(scale * f32(size))
199 ctx.ft.fons.set_align(int(cfg.align) | int(cfg.vertical_align))
200 color := sfons.rgba(cfg.color.r, cfg.color.g, cfg.color.b, cfg.color.a)
201 if cfg.color.a != 255 {
202 sgl.load_pipeline(ctx.pipeline.alpha)
203 }
204 ctx.ft.fons.set_color(color)
205 ascender := f32(0.0)
206 descender := f32(0.0)
207 lh := f32(0.0)
208 ctx.ft.fons.vert_metrics(&ascender, &descender, &lh)
209}
210
211@[params]
212pub struct DrawTextParams {
213pub:
214 x int
215 y int
216 text string
217
218 color Color = black
219 size int = 16
220 align HorizontalAlign = .left
221 vertical_align VerticalAlign = .top
222 max_width int
223 family string
224 bold bool
225 mono bool
226 italic bool
227}
228
229pub fn (ctx &Context) draw_text2(p DrawTextParams) {
230 ctx.draw_text(p.x, p.y, p.text, TextCfg{
231 color: p.color
232 size: p.size
233 align: p.align
234 vertical_align: p.vertical_align
235 max_width: p.max_width
236 family: p.family
237 bold: p.bold
238 mono: p.mono
239 italic: p.italic
240 }) // TODO: perf once it's the only function to draw text
241}
242
243// draw_text draws the string in `text_` starting at top-left position `x`,`y`.
244// Text settings can be provided with `cfg`.
245pub fn (ctx &Context) draw_text(x int, y int, text_ string, cfg TextCfg) {
246 $if macos {
247 if ctx.native_rendering {
248 if cfg.align == align_right {
249 width := ctx.text_width(text_)
250 // println('draw text ctx.height = ${ctx.height}')
251 C.darwin_draw_string(x - width, ctx.height - y, text_, cfg)
252 } else {
253 C.darwin_draw_string(x, ctx.height - y, text_, cfg)
254 }
255 return
256 }
257 }
258 if !ctx.font_inited {
259 eprintln('gg: draw_text(): font not initialized')
260 return
261 }
262 // text := text_.trim_space() // TODO: remove/optimize
263 // mut text := text_
264 // if text.contains('\t') {
265 // text = text.replace('\t', ' ')
266 // }
267 ctx.set_text_cfg(cfg)
268 scale := if ctx.ft.scale == 0 { f32(1) } else { ctx.ft.scale }
269 ctx.ft.fons.draw_text(x * scale, y * scale, text_) // TODO: check offsets/alignment
270}
271
272// draw_text draws the string in `text_` starting at top-left position `x`,`y` using
273// default text settings.
274pub fn (ctx &Context) draw_text_def(x int, y int, text string) {
275 ctx.draw_text(x, y, text)
276}
277
278// flush prepares the font for use.
279pub fn (ft &FT) flush() {
280 sfons.flush(ft.fons)
281}
282
283@[inline]
284fn (ctx &Context) text_metrics(s string) (f32, [4]f32) {
285 mut bounds := [4]f32{}
286 advance := ctx.ft.fons.text_bounds(0, 0, s, &bounds[0])
287 return advance, bounds
288}
289
290// text_width returns the width of the `string` `s` in pixels.
291pub fn (ctx &Context) text_width(s string) int {
292 $if macos {
293 if ctx.native_rendering {
294 return C.darwin_text_width(s)
295 }
296 }
297 // ctx.set_text_cfg(cfg) TODO
298 if !ctx.font_inited {
299 return 0
300 }
301 advance, _ := ctx.text_metrics(s)
302 return int(advance / ctx.scale)
303}
304
305// text_height returns the height of the `string` `s` in pixels.
306pub fn (ctx &Context) text_height(s string) int {
307 // ctx.set_text_cfg(cfg) TODO
308 if !ctx.font_inited {
309 return 0
310 }
311 _, bounds := ctx.text_metrics(s)
312 return int((bounds[3] - bounds[1]) / ctx.scale)
313}
314
315// text_size returns the width and height of the `string` `s` in pixels.
316pub fn (ctx &Context) text_size(s string) (int, int) {
317 // ctx.set_text_cfg(cfg) TODO
318 if !ctx.font_inited {
319 return 0, 0
320 }
321 advance, bounds := ctx.text_metrics(s)
322 return int(advance / ctx.scale), int((bounds[3] - bounds[1]) / ctx.scale)
323}
324
325// text_width returns the width of the `string` `s` in pixels.
326pub fn (ctx &Context) text_width_f(s string) f32 {
327 $if macos {
328 if ctx.native_rendering {
329 return C.darwin_text_width(s)
330 }
331 }
332 // ctx.set_text_cfg(cfg) TODO
333 if !ctx.font_inited {
334 return 0
335 }
336 advance, _ := ctx.text_metrics(s)
337 return advance / ctx.scale
338}
339