summaryrefslogtreecommitdiff
path: root/code/libs/handmade_font.h
diff options
context:
space:
mode:
Diffstat (limited to 'code/libs/handmade_font.h')
-rw-r--r--code/libs/handmade_font.h353
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