| 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. |
| 3 | module gg |
| 4 | |
| 5 | import fontstash |
| 6 | import sokol.sfons |
| 7 | import sokol.sgl |
| 8 | import os |
| 9 | import os.font |
| 10 | |
| 11 | pub struct FT { |
| 12 | pub: |
| 13 | fons &fontstash.Context = unsafe { nil } |
| 14 | font_normal int |
| 15 | font_bold int |
| 16 | font_mono int |
| 17 | font_italic int |
| 18 | pub 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 | |
| 23 | pub enum HorizontalAlign { |
| 24 | left = C.FONS_ALIGN_LEFT |
| 25 | center = C.FONS_ALIGN_CENTER |
| 26 | right = C.FONS_ALIGN_RIGHT |
| 27 | } |
| 28 | |
| 29 | pub 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 | |
| 36 | const initial_text_atlas_size = int($d('gg_text_buff_size', 2048)) |
| 37 | const max_text_atlas_size = 8192 |
| 38 | |
| 39 | fn 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 | |
| 67 | fn 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 |
| 168 | pub 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] |
| 212 | pub struct DrawTextParams { |
| 213 | pub: |
| 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 | |
| 229 | pub 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`. |
| 245 | pub 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. |
| 274 | pub 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. |
| 279 | pub fn (ft &FT) flush() { |
| 280 | sfons.flush(ft.fons) |
| 281 | } |
| 282 | |
| 283 | @[inline] |
| 284 | fn (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. |
| 291 | pub 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. |
| 306 | pub 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. |
| 316 | pub 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. |
| 326 | pub 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 | |