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.. 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.. 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 }