Harbor

branch main
showing the latest snapshot on main
layout.odin 6.4 KB · Plain text
layout.odin 0644 Raw
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
}