Checkpoint 2: integrate native Editor, MCP, gameplay builds and standalone export
This commit is contained in:
+171
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user