diff options
| author | Raymaekers Luca <luca@spacehb.net> | 2025-10-10 11:29:09 +0200 |
|---|---|---|
| committer | Raymaekers Luca <luca@spacehb.net> | 2025-10-10 11:29:09 +0200 |
| commit | 7cb810089144d42d202e0b267235174d7869c5aa (patch) | |
| tree | 579008fbb822bc78412e3604909e94a4711cfeaf /code/libs/handmade_font.h | |
| parent | 732e6a47ded496cd224e1cfadc2106b04dcf6859 (diff) | |
checkpoint
Diffstat (limited to 'code/libs/handmade_font.h')
| -rw-r--r-- | code/libs/handmade_font.h | 353 |
1 files changed, 353 insertions, 0 deletions
diff --git a/code/libs/handmade_font.h b/code/libs/handmade_font.h new file mode 100644 index 0000000..6711e09 --- /dev/null +++ b/code/libs/handmade_font.h @@ -0,0 +1,353 @@ +#ifndef HANDMADE_FONT_H +#define HANDMADE_FONT_H +#include "stb_truetype.h" + +struct game_font +{ + stbtt_fontinfo Info; + s32 Ascent; + s32 Descent; + s32 LineGap; + v2 BoundingBox[2]; + b32 Initialized; // For debugging. +}; + +#endif //HANDMADE_FONT_H + +#ifdef HANDMADE_FONT_IMPLEMENTATION +//~ Libraries +#define STB_TRUETYPE_IMPLEMENTATION +#include "stb_truetype.h" + +#define RoundReal32ToInt32 round +#define FloorReal32ToInt32 floor +#define free Free + +//~ Implementation +//- Loading +internal void +InitFontFromBuffer(game_font *Font, char *Buffer) +{ + if(Buffer) + { + if(stbtt_InitFont(&Font->Info, (u8 *)Buffer, stbtt_GetFontOffsetForIndex((u8 *)Buffer, 0))) + { + Font->Info.data = (u8 *)Buffer; + + s32 X0, Y0, X1, Y1; + stbtt_GetFontBoundingBox(&Font->Info, &X0, &Y0, &X1, &Y1); + Font->BoundingBox[0] = v2{(r32)X0, (r32)Y0}; + Font->BoundingBox[1] = v2{(r32)X1, (r32)Y1}; + stbtt_GetFontVMetrics(&Font->Info, &Font->Ascent, &Font->Descent, &Font->LineGap); + Font->Initialized = true; + } + else + { + // TODO(luca): Logging + } + } + else + { + // TODO(luca): Logging + } +} + +//- Rendering +internal void +DrawCharacter(game_offscreen_buffer *Buffer, u8 *FontBitmap, + int FontWidth, int FontHeight, + int XOffset, int YOffset, + v3 Color) +{ + s32 MinX = 0; + s32 MinY = 0; + s32 MaxX = FontWidth; + s32 MaxY = FontHeight; + + if(XOffset < 0) + { + MinX = -XOffset; + XOffset = 0; + } + if(YOffset < 0) + { + MinY = -YOffset; + YOffset = 0; + } + if(XOffset + FontWidth > Buffer->Width) + { + MaxX -= ((XOffset + FontWidth) - Buffer->Width); + } + if(YOffset + FontHeight > Buffer->Height) + { + MaxY -= ((YOffset + FontHeight) - Buffer->Height); + } + + u8 *Row = (u8 *)(Buffer->Memory) + + (YOffset*Buffer->Pitch) + + (XOffset*Buffer->BytesPerPixel); + + for(int Y = MinY; + Y < MaxY; + Y++) + { + u32 *Pixel = (u32 *)Row; + for(int X = MinX; + X < MaxX; + X++) + { + u8 Brightness = FontBitmap[Y*FontWidth+X]; + r32 Alpha = ((r32)Brightness/255.0f); + + r32 DR = (r32)((*Pixel >> 16) & 0xFF); + r32 DG = (r32)((*Pixel >> 8) & 0xFF); + r32 DB = (r32)((*Pixel >> 0) & 0xFF); + + r32 R = Color.R*255.0f*Alpha + DR*(1-Alpha); + r32 G = Color.G*255.0f*Alpha + DG*(1-Alpha); + r32 B = Color.B*255.0f*Alpha + DB*(1-Alpha); + + u32 Value = ((0xFF << 24) | + ((u32)(R) << 16) | + ((u32)(G) << 8) | + ((u32)(B) << 0)); + *Pixel++ = Value; + } + + Row += Buffer->Pitch; + } +} + + +internal void +DrawText(game_offscreen_buffer *Buffer, game_font *Font, r32 HeightPixels, + u32 TextLen, u8 *Text, v2 Offset, v3 Color, b32 IsUTF8) +{ + Assert(Font->Initialized); + + Offset.X = RoundReal32ToInt32(Offset.X); + Offset.Y = RoundReal32ToInt32(Offset.Y); + + r32 FontScale = stbtt_ScaleForPixelHeight(&Font->Info, HeightPixels); + + for(u32 TextIndex = 0; + TextIndex < TextLen; + TextIndex++) + { + rune CharAt = 0; + if(IsUTF8) + { + CharAt = *(((rune *)Text) + TextIndex); + } + else + { + CharAt = Text[TextIndex]; + } + + s32 FontWidth, FontHeight; + s32 AdvanceWidth, LeftSideBearing; + s32 X0, Y0, X1, Y1; + u8 *FontBitmap = 0; + // TODO(luca): Get rid of malloc. + FontBitmap = stbtt_GetCodepointBitmap(&Font->Info, + FontScale, FontScale, + CharAt, + &FontWidth, &FontHeight, 0, 0); + stbtt_GetCodepointBitmapBox(&Font->Info, CharAt, + FontScale, FontScale, + &X0, &Y0, &X1, &Y1); + stbtt_GetCodepointHMetrics(&Font->Info, CharAt, &AdvanceWidth, &LeftSideBearing); + + s32 XOffset = FloorReal32ToInt32(Offset.X + LeftSideBearing*FontScale); + s32 YOffset = Offset.Y + Y0; + + DrawCharacter(Buffer, FontBitmap, FontWidth, FontHeight, XOffset, YOffset, Color); + + Offset.X += RoundReal32ToInt32(AdvanceWidth*FontScale); + free(FontBitmap); + } +} + +// 1. First pass where we check each character's size. +// 2. Save positions where we need to wrap. +// Wrapping algorithm +// 1. When a character exceeds the box maximum width search backwards for whitespace. +// I. Whitespace found? +// Y -> Length of string until whitespace would fit? +// Y -> Save whitespace's position. This becomes the new searching start position. +// N -> Break on the character that exceeds the maximum width. +// N -> Break on the character that exceeds the maximum width. +// II. Continue until end of string. + +internal void +DrawTextInBox(memory_arena *Arena, game_offscreen_buffer *Buffer, game_font *Font, + string Text, r32 HeightPx, v3 Color, + v2 BoxMin, v2 BoxMax, b32 Centered) +{ + psize ArenaStartUsed = Arena->Used; + s32 *CharacterPixelWidths = PushArray(Arena, Text.Count, s32); + u32 *WrapPositions = PushArray(Arena, 0, u32); + u32 WrapPositionsCount = 0; + + r32 FontScale = stbtt_ScaleForPixelHeight(&Font->Info, HeightPx); + + // TODO(luca): UTF8 support + // e.g. (https://en.wikipedia.org/wiki/Whitespace_character) these are all whitespace characters that we might want to support. + for(u32 TextIndex = 0; + TextIndex < Text.Count; + TextIndex++) + { + u8 CharAt = Text.Data[TextIndex]; + + s32 AdvanceWidth, LeftSideBearing; + stbtt_GetCodepointHMetrics(&Font->Info, CharAt, &AdvanceWidth, &LeftSideBearing); + + CharacterPixelWidths[TextIndex] = RoundReal32ToInt32(FontScale*AdvanceWidth); + } + + s32 MaxWidth = BoxMax.X - BoxMin.X; + Assert(MaxWidth >= 0); + + u32 SearchStart = 0; + while(SearchStart < Text.Count) + { + s32 CumulatedWidth = 0; + u32 SearchIndex = SearchStart; + for(; + ((SearchIndex < Text.Count) && + (CumulatedWidth <= MaxWidth)); + SearchIndex++) + { + s32 Width = CharacterPixelWidths[SearchIndex]; + CumulatedWidth += Width; + } + + if(CumulatedWidth > MaxWidth) + { + // We need to search backwards for wrapping. + SearchIndex--; + u32 SearchIndexStop = SearchIndex; + + while(SearchIndex > SearchStart) + { + if(Text.Data[SearchIndex] == ' ') + { + PushArray(Arena, 1, u32); + WrapPositions[WrapPositionsCount++] = SearchIndex; + break; + } + + SearchIndex--; + } + + if(SearchIndex > SearchStart) + { + Assert(SearchIndex > SearchStart); + // luca: We skip the character we wrapped on. + SearchStart = SearchIndex + 1; + } + else if(SearchIndex == SearchStart) + { + Assert(SearchIndexStop > SearchStart); + PushArray(Arena, 1, u32); + WrapPositions[WrapPositionsCount++] = SearchIndexStop; + SearchStart = SearchIndexStop; + } + else + { + Assert(0); + } + + } + else + { + // luca: We don't need to wrap, we've reached the end of the text. + break; + } + } + + s32 YAdvance = FontScale*(Font->Ascent - Font->Descent + + Font->LineGap); + + v2 TextOffset = BoxMin; + + // Add baseline + TextOffset.Y += FontScale*Font->Ascent; + + if(Centered) + { + s32 TextHeight = YAdvance * (WrapPositionsCount + 1); + s32 CenterHOffset = ((BoxMax.Y - BoxMin.Y) - TextHeight)/2; + if(CenterHOffset >= 0) + { + TextOffset.Y += CenterHOffset; + } + } + + u32 Start = 0; + for(u32 WrapIndex = 0; + WrapIndex < WrapPositionsCount; + WrapIndex++) + { + u32 Position = WrapPositions[WrapIndex]; + + if(TextOffset.Y - FontScale*Font->Descent < BoxMax.Y) + { + + b32 DoCenter = (Centered && + ((WrapIndex == 0) || + (Text.Data[(WrapPositions[WrapIndex - 1])] == ' '))); + if(DoCenter) + { + s32 TextWidth = 0; + for(u32 WidthIndex = Start; + WidthIndex < Position; + WidthIndex++) + { + TextWidth += CharacterPixelWidths[WidthIndex]; + } + TextOffset.X = BoxMin.X + ((MaxWidth - TextWidth)/2); + } + + DrawText(Buffer, Font, HeightPx, + Position - Start, Text.Data + Start, + TextOffset, Color, false); + } + + TextOffset.Y += YAdvance; + + if(Text.Data[Position] == ' ') + { + Position++; + } + + Start = Position; + } + + TextOffset.X = BoxMin.X; + + b32 DoCenter = (Centered && + ((WrapPositionsCount == 0) || (Text.Data[WrapPositions[WrapPositionsCount - 1]] == ' '))); + if(DoCenter) + { + s32 TextWidth = 0; + for(u32 WidthIndex = Start; + WidthIndex < Text.Count; + WidthIndex++) + { + TextWidth += CharacterPixelWidths[WidthIndex]; + } + TextOffset.X = BoxMin.X + ((MaxWidth - TextWidth)/2); + } + + if(TextOffset.Y - FontScale*Font->Descent < BoxMax.Y) + { + DrawText(Buffer, Font, HeightPx, + Text.Count - Start, Text.Data + Start, + TextOffset, Color, false); + } + + Arena->Used = ArenaStartUsed; +} + +#endif //HANDMADE_FONT_IMPLEMENTATION |
