feat: unicode emoji support + other stuff

This commit is contained in:
Kae 2024-04-24 07:44:53 +10:00
parent 6ac139321b
commit d0f8aec244
16 changed files with 162 additions and 74 deletions

Binary file not shown.

View File

@ -1,9 +1,14 @@
{
"font" : {
"defaultFont" : "hobo",
"fallbackFont" : "unifont",
"emojiFont" : "twemoji"
},
"nametag" : {
"showMasterNames" : true,
// border is double here as it otherwise fades out near the end
// (could just the end to opaque too, but then diagonals are more jaggy)
"textStyle" : { "backDirectives" : "?border=1;222;2224?border=1;222;2224" },
"textStyle" : { "backDirectives" : "border=1;222;2224?border=1;222;2224" },
"inspectOpacityRate" : 0.15,
"movementThreshold" : 0.5,
"offset" : [0, 13]
@ -19,7 +24,7 @@
"paneTextStyle" : {},
"textBoxTextStyle" : {},
"itemSlotTextStyle" : {
"backDirectives" : "?border=1;444;4444"
"backDirectives" : "border=1;444;4444"
},
"teamBarNameStyle" : {},
"cursorTooltip" : { "textStyle" : {} },
@ -27,7 +32,7 @@
"debugTextStyle" : {
"font" : "iosevka-semibold",
"fontSize" : 7,
"backDirectives" : "?border=2;111a;1114"
"backDirectives" : "border=1;333a;3334"
},
"debugSpatialClearTime" : 0.0,
"debugOffset" : [80, 130],
@ -35,7 +40,7 @@
// Change planet name to support the new internal string formatting.
"planetNameFormatString" : "- {} -",
"planetTextStyle" : {
"backDirectives" : "?border=6;000;000",
"backDirectives" : "border=6;000;000",
"fontSize" : 24
},

View File

@ -2,7 +2,8 @@
"config" : {
"lineHeight" : 1,
"padding" : [1, 1],
"textStyle" : { "backDirectives" : "?border=1;111a;1114" },
"textStyle" : { "backDirectives" : "border=1;111a;1117" },
"chatFormatString" : "^backdirectives=border=1=333=3337;<^backdirectives=border=1=333=3330?border=1=333=3337,white;{}^reset,backdirectives=border=1=333=3337;>^reset,#eee; {}",
"colors" : {
"local" : "^white;",
"party" : "^blue;",

View File

@ -7,7 +7,7 @@
namespace Star {
constexpr int FontLoadFlags = FT_LOAD_FORCE_AUTOHINT;
constexpr int FontLoadFlags = FT_LOAD_NO_SVG | FT_LOAD_COLOR;
struct FTContext {
FT_Library library;
@ -90,11 +90,12 @@ unsigned Font::width(String::Char c) {
}
std::pair<Image, Vec2I> Font::render(String::Char c) {
tuple<Image, Vec2I, bool> Font::render(String::Char c) {
if (!m_fontImpl)
throw FontException("Font::render called on uninitialized font.");
FT_Face face = m_fontImpl->face;
if (m_loadedPixelSize != m_pixelSize || m_loadedChar != c) {
FT_UInt glyph_index = FT_Get_Char_Index(face, c);
if (FT_Load_Glyph(face, glyph_index, FontLoadFlags) != 0)
return {};
@ -102,35 +103,57 @@ std::pair<Image, Vec2I> Font::render(String::Char c) {
/* convert to an anti-aliased bitmap */
if (FT_Render_Glyph(face->glyph, FT_RENDER_MODE_NORMAL) != 0)
return {};
}
m_loadedPixelSize = m_pixelSize;
m_loadedChar = m_loadedChar;
FT_GlyphSlot slot = face->glyph;
unsigned width = slot->bitmap.width;
unsigned height = slot->bitmap.rows;
bool colored = false;
Image image(width + 2, height + 2, PixelFormat::RGBA32);
Image image(width + 2, height + 2, PixelFormat::BGRA32);
if (slot->bitmap.pixel_mode == FT_PIXEL_MODE_GRAY) {
Vec4B white(255, 255, 255, 0);
image.fill(white);
for (unsigned y = 0; y != height; ++y) {
uint8_t* p = slot->bitmap.buffer + y * slot->bitmap.pitch;
for (unsigned x = 0; x != width; ++x) {
if (x < width && y < height) {
uint8_t value = *(p + x);
if (m_alphaThreshold) {
if (value >= m_alphaThreshold) {
white[3] = 255;
image.set(x + 1, height - y, white);
}
} else if (value) {
white[3] = value;
white[3] = m_alphaThreshold
? (*(p + x) >= m_alphaThreshold ? 255 : 0)
: *(p + x);
image.set(x + 1, height - y, white);
}
}
}
} else if (colored = slot->bitmap.pixel_mode == FT_PIXEL_MODE_BGRA) {
unsigned bpp = image.bytesPerPixel();
uint8_t* data = image.data() + bpp + ((image.width() * (image.height() - 2)) * bpp); // offset by 1 pixel as it's padded
for (size_t y = 0; y != height; ++y) {
memcpy(data - (y * image.width() * bpp),
slot->bitmap.buffer + y * slot->bitmap.pitch,
slot->bitmap.pitch);
}
// unfortunately FreeType pre-multiplied the color channels :(
image.forEachPixel([](unsigned, unsigned, Vec4B& pixel) {
if (pixel[3] != 0 && pixel[3] != 255) {
float a = byteToFloat(pixel[3]);
pixel[0] /= a;
pixel[1] /= a;
pixel[2] /= a;
}
});
} else {
return {};
}
return { std::move(image), {slot->bitmap_left - 1, (slot->bitmap_top - height) + (m_pixelSize / 4) - 1} };
return make_tuple(
std::move(image),
Vec2I(slot->bitmap_left - 1, (slot->bitmap_top - height) + (m_pixelSize / 4) - 1),
colored);
}
bool Font::exists(String::Char c) {

View File

@ -34,13 +34,15 @@ public:
// May return empty image on unrenderable character (Normally, this will
// render a box, but if there is an internal freetype error this may return
// an empty image).
std::pair<Image, Vec2I> render(String::Char c);
tuple<Image, Vec2I, bool> render(String::Char c);
bool exists(String::Char c);
private:
FontImplPtr m_fontImpl;
ByteArrayConstPtr m_fontBuffer;
unsigned m_pixelSize;
unsigned m_loadedPixelSize;
String::Char m_loadedChar;
uint8_t m_alphaThreshold;
HashMap<pair<String::Char, unsigned>, unsigned> m_widthCache;

View File

@ -90,7 +90,8 @@ void BaseScriptPane::tick(float dt) {
bool BaseScriptPane::sendEvent(InputEvent const& event) {
// Intercept GuiClose before the canvas child so GuiClose always closes
// BaseScriptPanes without having to support it in the script.
if (context()->actions(event).contains(InterfaceAction::GuiClose)) {
if (context()->actions(event).contains(InterfaceAction::GuiClose)
&& m_config.getBool("dismissable", true)) {
dismiss();
return true;
}

View File

@ -27,6 +27,7 @@ Chat::Chat(UniverseClientPtr client) : m_client(client) {
m_chatVisTime = config.get("visTime").toFloat();
m_fadeRate = config.get("fadeRate").toDouble();
m_chatHistoryLimit = config.get("chatHistoryLimit").toInt();
m_chatFormatString = config.getString("chatFormatString");
m_portraitTextOffset = jsonToVec2I(config.get("portraitTextOffset"));
m_portraitImageOffset = jsonToVec2I(config.get("portraitImageOffset"));
@ -180,7 +181,7 @@ void Chat::addMessages(List<ChatReceivedMessage> const& messages, bool showPane)
guiContext.setTextStyle(m_chatTextStyle);
StringList lines;
if (message.fromNick != "" && message.portrait == "")
lines = guiContext.wrapInterfaceText(strf("<{}> {}", message.fromNick, message.text), wrapWidth);
lines = guiContext.wrapInterfaceText(strf(m_chatFormatString.utf8Ptr(), message.fromNick, message.text), wrapWidth);
else
lines = guiContext.wrapInterfaceText(message.text, wrapWidth);
@ -203,7 +204,8 @@ void Chat::addMessages(List<ChatReceivedMessage> const& messages, bool showPane)
show();
}
m_receivedMessages.resize(std::min((unsigned)m_receivedMessages.size(), m_chatHistoryLimit));
if ((unsigned)m_receivedMessages.size() > m_chatHistoryLimit)
m_receivedMessages.resize(m_chatHistoryLimit);
}
void Chat::addHistory(String const& chat) {
@ -216,6 +218,13 @@ void Chat::addHistory(String const& chat) {
m_client->playerStorage()->setMetadata("chatHistory", JsonArray::from(m_chatHistory));
}
void Chat::clear(size_t count) {
if (count > m_receivedMessages.size())
m_receivedMessages.clear();
else
m_receivedMessages.erase(m_receivedMessages.begin(), m_receivedMessages.begin() + count);
}
void Chat::renderImpl() {
Pane::renderImpl();
if (m_textBox->hasFocus())

View File

@ -30,6 +30,7 @@ public:
void addLine(String const& text, bool showPane = true);
void addMessages(List<ChatReceivedMessage> const& messages, bool showPane = true);
void addHistory(String const& chat);
void clear(size_t count = std::numeric_limits<size_t>::max());
String currentChat() const;
bool setCurrentChat(String const& chat, bool moveCursor = false);
@ -69,6 +70,7 @@ private:
TextStyle m_chatTextStyle;
unsigned m_chatHistoryLimit;
int m_historyOffset;
String m_chatFormatString;
CanvasWidgetPtr m_chatLog;
Vec2I m_chatLogPadding;

View File

@ -13,6 +13,11 @@ namespace Star {
LuaCallbacks LuaBindings::makeInterfaceCallbacks(MainInterface* mainInterface) {
LuaCallbacks callbacks;
callbacks.registerCallbackWithSignature<bool>(
"hudVisible", bind(mem_fn(&MainInterface::hudVisible), mainInterface));
callbacks.registerCallbackWithSignature<void, bool>(
"setHudVisible", bind(mem_fn(&MainInterface::setHudVisible), mainInterface, _1));
callbacks.registerCallback("bindCanvas", [mainInterface](String const& canvasName, Maybe<bool> ignoreInterfaceScale) -> Maybe<CanvasWidgetPtr> {
if (auto canvas = mainInterface->fetchCanvas(canvasName, ignoreInterfaceScale.value(false)))
return canvas;
@ -87,6 +92,10 @@ LuaCallbacks LuaBindings::makeChatCallbacks(MainInterface* mainInterface, Univer
return chat->setCurrentChat(text, moveCursor.value(false));
});
callbacks.registerCallback("clear", [chat](Maybe<size_t> count) {
chat->clear(count.value(std::numeric_limits<size_t>::max()));
});
return callbacks;
}

View File

@ -893,6 +893,14 @@ bool MainInterface::fixedCamera() const {
return m_clientCommandProcessor->fixedCameraEnabled();
}
bool MainInterface::hudVisible() const {
return !m_disableHud;
}
void MainInterface::setHudVisible(bool visible) {
m_disableHud = !visible;
}
void MainInterface::warpToOrbitedWorld(bool deploy) {
if (m_client->canBeamDown(deploy)) {
if (deploy)

View File

@ -100,7 +100,6 @@ public:
Vec2F cursorWorldPosition() const;
void toggleDebugDisplay();
bool isDebugDisplayed();
void doChat(String const& chat, bool addToHistory);
@ -112,6 +111,8 @@ public:
void queueJoinRequest(pair<String, RpcPromiseKeeper<P2PJoinRequestReply>> request);
bool fixedCamera() const;
bool hudVisible() const;
void setHudVisible(bool visible = true);
void warpToOrbitedWorld(bool deploy = false);
void warpToOwnShip();
@ -209,7 +210,7 @@ private:
QuestIndicatorPainterPtr m_questIndicatorPainter;
ChatBubbleManagerPtr m_chatBubbleManager;
bool m_disableHud{false};
bool m_disableHud = false;
String m_lastCommand;

View File

@ -115,7 +115,7 @@ Maybe<Uuid> PlayerStorage::playerUuidByName(String const& name, Maybe<Uuid> exce
RecursiveMutexLocker locker(m_mutex);
size_t longest = SIZE_MAX;
size_t longest = std::numeric_limits<size_t>::max();
for (auto& cache : m_savedPlayersCache) {
if (except && *except == cache.first)
continue;

View File

@ -14,13 +14,13 @@ void FontTextureGroup::cleanup(int64_t timeout) {
void FontTextureGroup::switchFont(String const& font) {
if (font.empty()) {
m_font = m_defaultFont;
m_activeFont = m_defaultFont;
m_fontName.clear();
}
else if (m_fontName != font) {
m_fontName = font;
auto find = m_fonts.find(font);
m_font = find != m_fonts.end() ? find->second : m_defaultFont;
m_activeFont = find != m_fonts.end() ? find->second : m_defaultFont;
}
}
@ -28,31 +28,35 @@ String const& FontTextureGroup::activeFont() {
return m_fontName;
}
void FontTextureGroup::addFont(FontPtr const& font, String const& name, bool isDefault) {
void FontTextureGroup::addFont(FontPtr const& font, String const& name) {
m_fonts[name] = font;
if (isDefault)
m_defaultFont = m_font = font;
}
void FontTextureGroup::clearFonts() {
m_fonts.clear();
m_font = m_defaultFont;
m_activeFont = m_defaultFont;
}
void FontTextureGroup::setFallbackFont(String const& fontName) {
if (auto font = m_fonts.ptr(fontName))
m_fallbackFont = *font;
void FontTextureGroup::setFixedFonts(String const& defaultFontName, String const& fallbackFontName, String const& emojiFontName) {
if (auto defaultFont = m_fonts.ptr(defaultFontName))
m_defaultFont = m_activeFont = *defaultFont;
if (auto fallbackFont = m_fonts.ptr(fallbackFontName))
m_fallbackFont = *fallbackFont;
if (auto emojiFont = m_fonts.ptr(emojiFontName))
m_emojiFont = *emojiFont;
}
const FontTextureGroup::GlyphTexture& FontTextureGroup::glyphTexture(String::Char c, unsigned size, Directives const* processingDirectives)
{
Font* font = (m_font->exists(c) || !m_fallbackFont) ? m_font.get() : m_fallbackFont.get();
Font* font = getFontForCharacter(c);
if (font == m_emojiFont.get())
processingDirectives = nullptr;
auto res = m_glyphs.insert(GlyphDescriptor{c, size, processingDirectives ? processingDirectives->hash() : 0, font}, GlyphTexture());
auto& glyphTexture = res.first->second;
if (res.second) {
font->setPixelSize(size);
auto pair = font->render(c);
Image& image = pair.first;
auto renderResult = font->render(c);
Image& image = get<0>(renderResult);
if (processingDirectives) {
try {
Directives const& directives = *processingDirectives;
@ -61,7 +65,7 @@ const FontTextureGroup::GlyphTexture& FontTextureGroup::glyphTexture(String::Cha
for (auto& entry : directives->entries)
processImageOperation(entry.operation, image);
res.first->second.offset = (preSize - Vec2F(image.size())) / 2;
glyphTexture.offset = (preSize - Vec2F(image.size())) / 2;
}
catch (StarException const&) {
image.forEachPixel([](unsigned x, unsigned y, Vec4B& pixel) {
@ -69,15 +73,14 @@ const FontTextureGroup::GlyphTexture& FontTextureGroup::glyphTexture(String::Cha
});
}
}
else
res.first->second.offset = Vec2F();
res.first->second.offset += Vec2F(pair.second);
res.first->second.texture = m_textureGroup->create(image);
glyphTexture.colored = get<2>(renderResult);
glyphTexture.offset += Vec2F(get<1>(renderResult));
glyphTexture.texture = m_textureGroup->create(image);
}
res.first->second.time = Time::monotonicMilliseconds();
return res.first->second;
glyphTexture.time = Time::monotonicMilliseconds();
return glyphTexture;
}
TexturePtr FontTextureGroup::glyphTexturePtr(String::Char c, unsigned size) {
@ -89,9 +92,31 @@ TexturePtr FontTextureGroup::glyphTexturePtr(String::Char c, unsigned size, Dire
}
unsigned FontTextureGroup::glyphWidth(String::Char c, unsigned fontSize) {
Font* font = (m_font->exists(c) || !m_fallbackFont) ? m_font.get() : m_fallbackFont.get();
Font* font = getFontForCharacter(c);
font->setPixelSize(fontSize);
return font->width(c);
}
Font* FontTextureGroup::getFontForCharacter(String::Char c) {
if (((c >= 0x1F600 && c <= 0x1F64F) || // Emoticons
(c >= 0x1F300 && c <= 0x1F5FF) || // Misc Symbols and Pictographs
(c >= 0x1F680 && c <= 0x1F6FF) || // Transport and Map
(c >= 0x1F1E6 && c <= 0x1F1FF) || // Regional country flags
(c >= 0x2600 && c <= 0x26FF ) || // Misc symbols 9728 - 9983
(c >= 0x2700 && c <= 0x27BF ) || // Dingbats
(c >= 0xFE00 && c <= 0xFE0F ) || // Variation Selectors
(c >= 0x1F900 && c <= 0x1F9FF) || // Supplemental Symbols and Pictographs
(c >= 0x1F018 && c <= 0x1F270) || // Various asian characters
(c >= 65024 && c <= 65039 ) || // Variation selector
(c >= 9100 && c <= 9300 ) || // Misc items
(c >= 8400 && c <= 8447 ))&& // Combining Diacritical Marks for Symbols
m_emojiFont->exists(c)
)
return m_emojiFont.get();
else if (m_activeFont->exists(c) || !m_fallbackFont)
return m_activeFont.get();
else
return m_fallbackFont.get();
}
}

View File

@ -16,7 +16,8 @@ public:
struct GlyphTexture {
TexturePtr texture;
int64_t time;
bool colored = false;
int64_t time = 0;
Vec2F offset;
};
@ -35,16 +36,18 @@ public:
// Switches the current font
void switchFont(String const& font);
String const& activeFont();
void addFont(FontPtr const& font, String const& name, bool isDefault = false);
void addFont(FontPtr const& font, String const& name);
void clearFonts();
void setFallbackFont(String const& fontName);
void setFixedFonts(String const& defaultFontName, String const& fallbackFontName, String const& emojiFontName);
private:
Font* getFontForCharacter(String::Char);
CaseInsensitiveStringMap<FontPtr> m_fonts;
String m_fontName;
FontPtr m_font;
FontPtr m_activeFont;
FontPtr m_defaultFont;
FontPtr m_fallbackFont;
FontPtr m_emojiFont;
TextureGroupPtr m_textureGroup;
HashMap<GlyphDescriptor, GlyphTexture> m_glyphs;

View File

@ -320,14 +320,9 @@ void TextPainter::reloadFonts() {
m_fontTextureGroup.clearFonts();
m_fontTextureGroup.cleanup(0);
auto assets = Root::singleton().assets();
String defaultName = "hobo";
auto defaultFont = loadFont("/hobo.ttf", defaultName);
auto loadFontsByExtension = [&](String const& ext) {
for (auto& fontPath : assets->scanExtension(ext)) {
auto font = assets->font(fontPath);
if (font == defaultFont)
continue;
auto name = AssetPath::filename(fontPath);
name = name.substr(0, name.findLast("."));
addFont(loadFont(fontPath, name), name);
@ -335,8 +330,10 @@ void TextPainter::reloadFonts() {
};
loadFontsByExtension("ttf");
loadFontsByExtension("woff2");
m_fontTextureGroup.addFont(defaultFont, defaultName, true);
m_fontTextureGroup.setFallbackFont("unifont");
m_fontTextureGroup.setFixedFonts(
assets->json("/interface.config:font.defaultFont").toString(),
assets->json("/interface.config:font.fallbackFont").toString(),
assets->json("/interface.config:font.emojiFont").toString());
}
void TextPainter::cleanup(int64_t timeout) {
@ -518,13 +515,15 @@ void TextPainter::renderPrimitives() {
}
void TextPainter::renderGlyph(String::Char c, Vec2F const& screenPos, List<RenderPrimitive>& out, unsigned fontSize,
float scale, Vec4B const& color, Directives const* processingDirectives) {
float scale, Vec4B color, Directives const* processingDirectives) {
if (!fontSize)
return;
const FontTextureGroup::GlyphTexture& glyphTexture = m_fontTextureGroup.glyphTexture(c, fontSize, processingDirectives);
Vec2F offset = glyphTexture.offset * scale;
out.emplace_back(std::in_place_type_t<RenderQuad>(), glyphTexture.texture, Vec2F::round(screenPos + offset), scale, color, 0.0f);
if (glyphTexture.colored)
color[0] = color[1] = color[2] = 255;
out.emplace_back(std::in_place_type_t<RenderQuad>(),
glyphTexture.texture, Vec2F::round(screenPos + glyphTexture.offset * scale), scale, color, 0.0f);
}
FontPtr TextPainter::loadFont(String const& fontPath, Maybe<String> fontName) {

View File

@ -86,7 +86,7 @@ private:
RectF doRenderGlyph(String::Char c, TextPositioning const& position, bool reallyRender);
void renderPrimitives();
void renderGlyph(String::Char c, Vec2F const& screenPos, List<RenderPrimitive>& out, unsigned fontSize, float scale, Vec4B const& color, Directives const* processingDirectives = nullptr);
void renderGlyph(String::Char c, Vec2F const& screenPos, List<RenderPrimitive>& out, unsigned fontSize, float scale, Vec4B color, Directives const* processingDirectives = nullptr);
static FontPtr loadFont(String const& fontPath, Maybe<String> fontName = {});
RendererPtr m_renderer;