Checkpoint 2: integrate native Editor, MCP, gameplay builds and standalone export

This commit is contained in:
Emil
2026-09-18 03:40:15 +03:00
parent 5c6b24d34d
commit 999686a896
125 changed files with 16086 additions and 1714 deletions
+171
View File
@@ -0,0 +1,171 @@
#include <faset/ui/ui.hpp>
#include <ft2build.h>
#include FT_FREETYPE_H
#include <algorithm>
#include <cmath>
#include <fstream>
#include <hb-ft.h>
#include <hb.h>
#include <map>
#include <stdexcept>
namespace faset::ui {
namespace {
void quad(render::Snapshot& snapshot, render::Quad q, const Rect& clip) {
const Rect area{q.x, q.y, q.width, q.height};
const auto visible = area.intersection(clip);
if (visible.width <= 0 || visible.height <= 0 || q.width <= 0 || q.height <= 0)
return;
const float u0 = q.uv_rect[0], v0 = q.uv_rect[1], du = q.uv_rect[2] - u0,
dv = q.uv_rect[3] - v0;
q.uv_rect = {u0 + (visible.x - q.x) / q.width * du, v0 + (visible.y - q.y) / q.height * dv,
u0 + (visible.x + visible.width - q.x) / q.width * du,
v0 + (visible.y + visible.height - q.y) / q.height * dv};
q.x = visible.x;
q.y = visible.y;
q.width = visible.width;
q.height = visible.height;
snapshot.ui_quads.push_back(std::move(q));
}
} // namespace
struct FontAtlas::Impl {
FT_Library library{};
FT_Face face{};
hb_font_t* font{};
std::vector<unsigned char> font_bytes;
std::shared_ptr<render::Texture> atlas = std::make_shared<render::Texture>();
struct Glyph {
unsigned x, y, width, height;
int left, top;
};
std::map<std::pair<unsigned, unsigned>, Glyph> glyphs;
unsigned x = 2, y = 2, row_height = 0, size = 0;
explicit Impl(const std::filesystem::path& path) {
std::ifstream in(path, std::ios::binary | std::ios::ate);
if (!in)
throw std::runtime_error("Cannot open UI font: " + path.string());
auto length = in.tellg();
if (length <= 0)
throw std::runtime_error("Empty UI font");
font_bytes.resize(static_cast<std::size_t>(length));
in.seekg(0);
in.read(reinterpret_cast<char*>(font_bytes.data()), length);
if (!in)
throw std::runtime_error("Cannot read UI font");
if (FT_Init_FreeType(&library))
throw std::runtime_error("FreeType initialization failed");
if (FT_New_Memory_Face(library, font_bytes.data(), static_cast<FT_Long>(font_bytes.size()),
0, &face)) {
FT_Done_FreeType(library);
library = nullptr;
throw std::runtime_error("Cannot load UI font face");
}
font = hb_ft_font_create_referenced(face);
hb_ft_font_set_load_flags(font, FT_LOAD_DEFAULT);
atlas->width = atlas->height = 2048;
atlas->rgba.resize(2048 * 2048 * 4, 0);
atlas->revision = 1;
atlas->srgb = false;
}
~Impl() {
if (font)
hb_font_destroy(font);
if (face)
FT_Done_Face(face);
if (library)
FT_Done_FreeType(library);
}
void set_size(float pixels) {
const auto wanted = static_cast<unsigned>(std::clamp(std::round(pixels), 6.f, 128.f));
if (wanted != size) {
size = wanted;
if (FT_Set_Pixel_Sizes(face, 0, size))
throw std::runtime_error("Invalid font size");
hb_ft_font_changed(font);
}
}
Glyph glyph(unsigned index) {
const auto key = std::make_pair(size, index);
if (auto found = glyphs.find(key); found != glyphs.end())
return found->second;
if (FT_Load_Glyph(face, index, FT_LOAD_DEFAULT) ||
FT_Render_Glyph(face->glyph, FT_RENDER_MODE_NORMAL))
throw std::runtime_error("Cannot rasterize UI glyph");
const auto& bitmap = face->glyph->bitmap;
if (x + bitmap.width + 2 > atlas->width) {
x = 2;
y += row_height + 2;
row_height = 0;
}
if (y + bitmap.rows + 2 > atlas->height)
throw std::runtime_error("UI font atlas exhausted");
Glyph result{
x, y, bitmap.width, bitmap.rows, face->glyph->bitmap_left, face->glyph->bitmap_top};
for (unsigned py = 0; py < bitmap.rows; ++py) {
const auto* row = bitmap.buffer + (bitmap.pitch >= 0 ? py : bitmap.rows - 1 - py) *
std::abs(bitmap.pitch);
for (unsigned px = 0; px < bitmap.width; ++px) {
const auto offset = ((y + py) * atlas->width + x + px) * 4;
atlas->rgba[offset] = atlas->rgba[offset + 1] = atlas->rgba[offset + 2] = 255;
atlas->rgba[offset + 3] = row[px];
}
}
x += bitmap.width + 2;
row_height = std::max(row_height, bitmap.rows);
++atlas->revision;
glyphs.emplace(key, result);
return result;
}
hb_buffer_t* shape(std::string_view text, float pixels) {
set_size(pixels);
auto* buffer = hb_buffer_create();
hb_buffer_add_utf8(buffer, text.data(), static_cast<int>(text.size()), 0,
static_cast<int>(text.size()));
hb_buffer_guess_segment_properties(buffer);
hb_shape(font, buffer, nullptr, 0);
return buffer;
}
};
FontAtlas::FontAtlas(const std::filesystem::path& path) : impl_(std::make_unique<Impl>(path)) {}
FontAtlas::~FontAtlas() = default;
float FontAtlas::measure(std::string_view text, float pixels) {
auto* buffer = impl_->shape(text, pixels);
std::unique_ptr<hb_buffer_t, decltype(&hb_buffer_destroy)> guard(buffer, &hb_buffer_destroy);
unsigned count = 0;
const auto* positions = hb_buffer_get_glyph_positions(buffer, &count);
float advance = 0;
for (unsigned i = 0; i < count; ++i)
advance += positions[i].x_advance / 64.f;
return advance;
}
void FontAtlas::draw(render::Snapshot& snapshot, std::string_view text, float x, float y,
float pixels, Color color, const Rect& clip) {
auto* buffer = impl_->shape(text, pixels);
std::unique_ptr<hb_buffer_t, decltype(&hb_buffer_destroy)> guard(buffer, &hb_buffer_destroy);
unsigned count = 0;
const auto* infos = hb_buffer_get_glyph_infos(buffer, &count);
const auto* positions = hb_buffer_get_glyph_positions(buffer, &count);
const float baseline = y + impl_->face->size->metrics.ascender / 64.f;
for (unsigned i = 0; i < count; ++i) {
const auto glyph = impl_->glyph(infos[i].codepoint);
if (glyph.width && glyph.height) {
render::Quad q;
q.x = x + positions[i].x_offset / 64.f + glyph.left;
q.y = baseline - positions[i].y_offset / 64.f - glyph.top;
q.width = static_cast<float>(glyph.width);
q.height = static_cast<float>(glyph.height);
q.color = color;
q.texture = impl_->atlas;
q.uv_rect = {float(glyph.x) / impl_->atlas->width,
float(glyph.y) / impl_->atlas->height,
float(glyph.x + glyph.width) / impl_->atlas->width,
float(glyph.y + glyph.height) / impl_->atlas->height};
quad(snapshot, std::move(q), clip);
}
x += positions[i].x_advance / 64.f;
}
}
std::shared_ptr<const render::Texture> FontAtlas::texture() const {
return impl_->atlas;
}
} // namespace faset::ui
+196
View File
@@ -0,0 +1,196 @@
#include <algorithm>
#include <cctype>
#include <faset/ui/ui.hpp>
#include <stdexcept>
namespace faset::ui {
namespace {
bool continuation(unsigned char c) {
return (c & 0xc0) == 0x80;
}
std::size_t previous(std::string_view text, std::size_t at) {
if (!at)
return 0;
--at;
while (at && continuation(static_cast<unsigned char>(text[at])))
--at;
return at;
}
std::size_t next(std::string_view text, std::size_t at) {
if (at >= text.size())
return text.size();
++at;
while (at < text.size() && continuation(static_cast<unsigned char>(text[at])))
++at;
return at;
}
bool word(unsigned char c) {
return c >= 128 || std::isalnum(c) || c == '_';
}
} // namespace
bool TextBuffer::valid_utf8(std::string_view text) {
for (std::size_t i = 0; i < text.size();) {
const auto lead = static_cast<unsigned char>(text[i++]);
if (lead < 128)
continue;
unsigned cp = 0;
int n = 0;
unsigned minimum = 0;
if ((lead & 0xe0) == 0xc0) {
cp = lead & 31;
n = 1;
minimum = 128;
} else if ((lead & 0xf0) == 0xe0) {
cp = lead & 15;
n = 2;
minimum = 2048;
} else if ((lead & 0xf8) == 0xf0) {
cp = lead & 7;
n = 3;
minimum = 65536;
} else
return false;
if (i + static_cast<std::size_t>(n) > text.size())
return false;
while (n--) {
auto c = static_cast<unsigned char>(text[i++]);
if (!continuation(c))
return false;
cp = (cp << 6) | (c & 63);
}
if (cp < minimum || cp > 0x10ffff || (cp >= 0xd800 && cp <= 0xdfff))
return false;
}
return true;
}
TextBuffer::TextBuffer(std::string text) {
reset(std::move(text));
}
void TextBuffer::reset(std::string text) {
if (!valid_utf8(text))
throw std::invalid_argument("Invalid UTF-8 text");
text_ = std::move(text);
cursor_ = anchor_ = text_.size();
undo_.clear();
redo_.clear();
}
void TextBuffer::set_cursor(std::size_t byte, bool select) {
cursor_ = std::min(byte, text_.size());
while (cursor_ && cursor_ < text_.size() &&
continuation(static_cast<unsigned char>(text_[cursor_])))
--cursor_;
if (!select)
anchor_ = cursor_;
}
void TextBuffer::select_all() {
anchor_ = 0;
cursor_ = text_.size();
}
std::string TextBuffer::selected_text() const {
return text_.substr(std::min(cursor_, anchor_),
std::max(cursor_, anchor_) - std::min(cursor_, anchor_));
}
void TextBuffer::left(bool select, bool by_word) {
if (!select && has_selection()) {
set_cursor(std::min(cursor_, anchor_));
return;
}
auto position = previous(text_, cursor_);
if (by_word) {
while (position && !word(static_cast<unsigned char>(text_[position])))
position = previous(text_, position);
while (position && word(static_cast<unsigned char>(text_[previous(text_, position)])))
position = previous(text_, position);
}
set_cursor(position, select);
}
void TextBuffer::right(bool select, bool by_word) {
if (!select && has_selection()) {
set_cursor(std::max(cursor_, anchor_));
return;
}
auto position = next(text_, cursor_);
if (by_word) {
while (position < text_.size() && word(static_cast<unsigned char>(text_[position])))
position = next(text_, position);
while (position < text_.size() && !word(static_cast<unsigned char>(text_[position])))
position = next(text_, position);
}
set_cursor(position, select);
}
void TextBuffer::home(bool select) {
set_cursor(0, select);
}
void TextBuffer::end(bool select) {
set_cursor(text_.size(), select);
}
void TextBuffer::remember() {
undo_.push_back({text_, cursor_, anchor_});
if (undo_.size() > 256)
undo_.erase(undo_.begin());
redo_.clear();
}
void TextBuffer::erase_selection() {
const auto begin = std::min(cursor_, anchor_), end = std::max(cursor_, anchor_);
text_.erase(begin, end - begin);
cursor_ = anchor_ = begin;
}
bool TextBuffer::insert(std::string_view utf8) {
if (!valid_utf8(utf8) || text_.size() + utf8.size() > 1024 * 1024)
return false;
if (utf8.empty() && !has_selection())
return false;
remember();
erase_selection();
text_.insert(cursor_, utf8);
cursor_ += utf8.size();
anchor_ = cursor_;
return true;
}
bool TextBuffer::backspace() {
if (!has_selection() && !cursor_)
return false;
remember();
if (has_selection())
erase_selection();
else {
auto begin = previous(text_, cursor_);
text_.erase(begin, cursor_ - begin);
cursor_ = anchor_ = begin;
}
return true;
}
bool TextBuffer::delete_forward() {
if (!has_selection() && cursor_ == text_.size())
return false;
remember();
if (has_selection())
erase_selection();
else
text_.erase(cursor_, next(text_, cursor_) - cursor_);
anchor_ = cursor_;
return true;
}
bool TextBuffer::undo() {
if (undo_.empty())
return false;
redo_.push_back({text_, cursor_, anchor_});
auto old = std::move(undo_.back());
undo_.pop_back();
text_ = std::move(old.text);
cursor_ = old.cursor;
anchor_ = old.anchor;
return true;
}
bool TextBuffer::redo() {
if (redo_.empty())
return false;
undo_.push_back({text_, cursor_, anchor_});
auto old = std::move(redo_.back());
redo_.pop_back();
text_ = std::move(old.text);
cursor_ = old.cursor;
anchor_ = old.anchor;
return true;
}
} // namespace faset::ui
+1149
View File
File diff suppressed because it is too large Load Diff