Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ set(SIMPLEGRAPHIC_SOURCES
"engine/core/core_tex_manipulation.h"
"engine/core/core_video.cpp"
"engine/core/core_video.h"
"engine/render/r_dynfont.h"
"engine/system/win/sys_ime.h"
"engine/render/r_font.cpp"
"engine/render/r_font.h"
"engine/render/r_main.cpp"
Expand Down Expand Up @@ -80,6 +82,8 @@ set (SIMPLEGRAPHIC_PLATFORM_SOURCES)
if (APPLE)
set (SIMPLEGRAPHIC_PLATFORM_SOURCES
"engine/system/win/sys_macos.mm"
"engine/system/win/sys_ime_macos.mm"
"engine/render/r_dynfont.mm"
)
endif()

Expand Down
8 changes: 8 additions & 0 deletions engine/core/core_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class core_main_c: public core_IMain {
void Frame();
void Shutdown();
void KeyEvent(int key, int type);
void PreeditEvent(const char* utf8Text, int caret);
bool CanExit();

// Encapsulated
Expand Down Expand Up @@ -102,6 +103,13 @@ void core_main_c::KeyEvent(int key, int type)
ui->KeyEvent(key, type);
}

void core_main_c::PreeditEvent(const char* utf8Text, int caret)
{
if ( !initialised ) return;

ui->PreeditEvent(utf8Text, caret);
}

bool core_main_c::CanExit()
{
if ( !initialised ) return true;
Expand Down
2 changes: 2 additions & 0 deletions engine/core/core_main.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ class core_IMain {
virtual void Frame() = 0;
virtual void Shutdown() = 0;
virtual void KeyEvent(int key, int type) = 0;
// Input method composition state; text is UTF-8, caret is a byte offset.
virtual void PreeditEvent(const char* utf8Text, int caret) = 0;
virtual bool CanExit() = 0;
};

2 changes: 2 additions & 0 deletions engine/render.h
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ class r_IRenderer {
virtual void SetDrawSubLayer(int subLayer) = 0;
virtual int GetDrawLayer() = 0;
virtual void SetViewport(int x = 0, int y = 0, int width = 0, int height = 0) = 0;
// Origin of the viewport currently being drawn into, in window space.
virtual void GetViewportOrigin(int& x, int& y) = 0;
virtual void SetBlendMode(int mode) = 0;
virtual void DrawColor(const col4_t col = NULL) = 0;
virtual void DrawColor(dword col) = 0;
Expand Down
35 changes: 35 additions & 0 deletions engine/render/r_dynfont.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// SimpleGraphic Engine
//
// Dynamic Glyph Rasterisation
//
// The bundled bitmap fonts only cover the first 128 codepoints. Anything
// outside that range used to be drawn as a "[U+XXXX]" tofu placeholder.
// This module rasterises arbitrary codepoints on demand using the platform
// font engine, which also gives us automatic font fallback (CJK, Cyrillic,
// emoji, ...) without having to ship or pick a font ourselves.
//
// Only implemented on macOS for now; other platforms report "no glyph" and
// keep the existing tofu behaviour.

#pragma once

#include <cstdint>
#include <vector>

struct dynGlyph_s {
int width = 0; // bitmap width in pixels
int height = 0; // bitmap height in pixels
int bearingX = 0; // x offset from the pen position to the bitmap's left edge
int bearingY = 0; // y offset from the baseline up to the bitmap's top edge
int advance = 0; // how far to move the pen after drawing
int ascent = 0; // distance from the top of the line box down to the baseline
std::vector<uint8_t> coverage; // width * height, 8-bit alpha coverage
};

// Rasterise one codepoint at the requested pixel size.
// Returns false when the platform has no glyph for it, or on any failure —
// callers should fall back to their existing placeholder rendering.
bool DynFontRasterize(char32_t cp, int pixelSize, dynGlyph_s& out);

// Whether dynamic rasterisation is available at all on this build.
bool DynFontAvailable();
153 changes: 153 additions & 0 deletions engine/render/r_dynfont.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// SimpleGraphic Engine
//
// Dynamic Glyph Rasterisation — macOS (CoreText) backend
//
// We deliberately go through CoreText rather than bundling a rasteriser and a
// font file: CTFontCreateForString gives us the system's font fallback chain,
// so a codepoint gets rendered with whatever installed font actually covers it
// (Songti/PingFang for Han, Hiragino for kana, Apple Color Emoji, ...) with no
// font list to curate.

#include "r_dynfont.h"

#include <CoreText/CoreText.h>
#include <CoreGraphics/CoreGraphics.h>

#include <algorithm>
#include <cmath>

namespace {

// Convert a codepoint to the UTF-16 units CoreText expects.
// Returns the number of units written (1 for the BMP, 2 for a surrogate pair).
int CodepointToUTF16(char32_t cp, UniChar out[2])
{
if (cp < 0x10000) {
out[0] = (UniChar)cp;
return 1;
}
char32_t v = cp - 0x10000;
out[0] = (UniChar)(0xD800 + (v >> 10));
out[1] = (UniChar)(0xDC00 + (v & 0x3FF));
return 2;
}

// Pick the font that actually has a glyph for this codepoint, starting from
// the standard UI font and letting CoreText walk the fallback chain.
// Returns a +1 reference the caller must release, or nullptr.
CTFontRef FontForCodepoint(char32_t cp, int pixelSize, CGGlyph& glyphOut)
{
UniChar units[2];
int unitCount = CodepointToUTF16(cp, units);

CTFontRef base = CTFontCreateUIFontForLanguage(kCTFontUIFontUser, (CGFloat)pixelSize, nullptr);
if (!base) {
return nullptr;
}

CGGlyph glyphs[2] = {0, 0};
if (CTFontGetGlyphsForCharacters(base, units, glyphs, unitCount) && glyphs[0]) {
glyphOut = glyphs[0];
return base;
}

// The base font can't render it — ask CoreText for a substitute.
CFStringRef str = CFStringCreateWithCharacters(nullptr, units, unitCount);
if (!str) {
CFRelease(base);
return nullptr;
}
CTFontRef fallback = CTFontCreateForString(base, str, CFRangeMake(0, unitCount));
CFRelease(str);
CFRelease(base);
if (!fallback) {
return nullptr;
}

glyphs[0] = glyphs[1] = 0;
if (!CTFontGetGlyphsForCharacters(fallback, units, glyphs, unitCount) || !glyphs[0]) {
CFRelease(fallback);
return nullptr;
}
glyphOut = glyphs[0];
return fallback;
}

} // namespace

bool DynFontAvailable()
{
return true;
}

bool DynFontRasterize(char32_t cp, int pixelSize, dynGlyph_s& out)
{
if (pixelSize <= 0 || pixelSize > 512) {
return false;
}

CGGlyph glyph = 0;
CTFontRef font = FontForCodepoint(cp, pixelSize, glyph);
if (!font) {
return false;
}

CGRect bounds = CTFontGetBoundingRectsForGlyphs(font, kCTFontOrientationHorizontal, &glyph, nullptr, 1);
CGSize advanceSize{};
CTFontGetAdvancesForGlyphs(font, kCTFontOrientationHorizontal, &glyph, &advanceSize, 1);

out = dynGlyph_s{};
out.advance = (int)std::ceil(advanceSize.width);
// The caller positions text by the top of the line box, so it needs to know
// where the baseline sits within it.
out.ascent = (int)std::ceil(CTFontGetAscent(font));

// Whitespace and other zero-area glyphs carry an advance but no pixels.
if (CGRectIsEmpty(bounds) || CGRectIsNull(bounds)) {
CFRelease(font);
return true;
}

// Pad by one pixel on each side so antialiased edges aren't clipped.
int x0 = (int)std::floor(CGRectGetMinX(bounds)) - 1;
int y0 = (int)std::floor(CGRectGetMinY(bounds)) - 1;
int x1 = (int)std::ceil(CGRectGetMaxX(bounds)) + 1;
int y1 = (int)std::ceil(CGRectGetMaxY(bounds)) + 1;

int w = x1 - x0;
int h = y1 - y0;
if (w <= 0 || h <= 0 || w > 1024 || h > 1024) {
CFRelease(font);
return false;
}

std::vector<uint8_t> pixels((size_t)w * h, 0);
CGContextRef ctx = CGBitmapContextCreate(pixels.data(), w, h, 8, w, nullptr, kCGImageAlphaOnly);
if (!ctx) {
CFRelease(font);
return false;
}

CGContextSetShouldAntialias(ctx, true);
CGContextSetShouldSmoothFonts(ctx, false); // grayscale AA; subpixel makes no sense in an alpha mask

// Place the glyph so its bounding box lands at the bitmap origin.
CGPoint pos = CGPointMake((CGFloat)-x0, (CGFloat)-y0);
CTFontDrawGlyphs(font, &glyph, &pos, 1, ctx);
CGContextFlush(ctx);
CGContextRelease(ctx);
CFRelease(font);

out.width = w;
out.height = h;
out.bearingX = x0;
// CoreGraphics y grows upwards; our renderer addresses rows downwards from
// the top, so the top edge sits y1 above the baseline.
out.bearingY = y1;
// A CGBitmapContext stores its first row at the top of the image even
// though its drawing origin is bottom-left, which already matches the
// renderer's top-down texture rows — no flip needed here.
out.coverage = std::move(pixels);

return true;
}
Loading