chore: sync from fn-registry agent
This commit is contained in:
@@ -0,0 +1,279 @@
|
||||
// Demos individuales de text_editor y file_watcher (Wave 1, issue 0025).
|
||||
//
|
||||
// Aunque las dos primitivas estan diseñadas para componerse, en gallery se
|
||||
// muestran por separado para que cada entry exhiba un solo primitivo y su
|
||||
// API minima.
|
||||
|
||||
#include "demos.h"
|
||||
#include "demo.h"
|
||||
|
||||
#include "core/text_editor.h"
|
||||
#include "core/file_watcher.h"
|
||||
#include "core/button.h"
|
||||
#include "core/tokens.h"
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <deque>
|
||||
#include <string>
|
||||
|
||||
namespace gallery {
|
||||
|
||||
// ===========================================================================
|
||||
// text_editor — editor de codigo con syntax highlighting
|
||||
// ===========================================================================
|
||||
|
||||
namespace {
|
||||
|
||||
const char* kSampleGLSL =
|
||||
"#version 330\n"
|
||||
"// fragment shader demo\n"
|
||||
"out vec4 frag_color;\n"
|
||||
"uniform vec2 u_resolution;\n"
|
||||
"uniform float u_time;\n"
|
||||
"\n"
|
||||
"void main() {\n"
|
||||
" vec2 uv = gl_FragCoord.xy / u_resolution;\n"
|
||||
" vec3 col = 0.5 + 0.5 * cos(u_time + uv.xyx + vec3(0,2,4));\n"
|
||||
" frag_color = vec4(col, 1.0);\n"
|
||||
"}\n";
|
||||
|
||||
const char* kSampleSQL =
|
||||
"-- fts5 search sobre el registry\n"
|
||||
"SELECT id, kind, purity, description\n"
|
||||
"FROM functions\n"
|
||||
"WHERE id IN (\n"
|
||||
" SELECT id FROM functions_fts\n"
|
||||
" WHERE functions_fts MATCH 'name:slic* OR description:slic*'\n"
|
||||
")\n"
|
||||
"ORDER BY name\n"
|
||||
"LIMIT 50;\n";
|
||||
|
||||
const char* kSampleCpp =
|
||||
"#include <imgui.h>\n"
|
||||
"namespace fn {\n"
|
||||
" bool button(const char* label, ButtonVariant v) {\n"
|
||||
" auto& tk = tokens::current();\n"
|
||||
" ImGui::PushStyleColor(ImGuiCol_Button, tk.bg_for(v));\n"
|
||||
" bool clicked = ImGui::Button(label);\n"
|
||||
" ImGui::PopStyleColor();\n"
|
||||
" return clicked;\n"
|
||||
" }\n"
|
||||
"}\n";
|
||||
|
||||
struct EditorState {
|
||||
fn::TextEditorState* ed = nullptr;
|
||||
fn::CodeLang lang = fn::CodeLang::GLSL;
|
||||
};
|
||||
|
||||
EditorState& editor_state() {
|
||||
static EditorState s;
|
||||
return s;
|
||||
}
|
||||
|
||||
void ensure_editor() {
|
||||
auto& s = editor_state();
|
||||
if (!s.ed) {
|
||||
s.ed = fn::text_editor_create(s.lang);
|
||||
fn::text_editor_set_text(s.ed, kSampleGLSL);
|
||||
}
|
||||
}
|
||||
|
||||
void apply_language(fn::CodeLang next) {
|
||||
auto& s = editor_state();
|
||||
if (next == s.lang) return;
|
||||
fn::text_editor_destroy(s.ed);
|
||||
s.ed = fn::text_editor_create(next);
|
||||
s.lang = next;
|
||||
switch (next) {
|
||||
case fn::CodeLang::GLSL: fn::text_editor_set_text(s.ed, kSampleGLSL); break;
|
||||
case fn::CodeLang::SQL: fn::text_editor_set_text(s.ed, kSampleSQL); break;
|
||||
case fn::CodeLang::Cpp: fn::text_editor_set_text(s.ed, kSampleCpp); break;
|
||||
case fn::CodeLang::Generic: fn::text_editor_set_text(s.ed, ""); break;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void demo_text_editor() {
|
||||
using namespace fn_tokens;
|
||||
|
||||
demo_header("text_editor", "v1.0.0",
|
||||
"Editor de codigo embebido en ImGui con syntax highlighting (GLSL/SQL/Cpp/Generic). "
|
||||
"Wrapper PIMPL sobre ImGuiColorTextEdit (MIT). API: create / set_text / get_text / "
|
||||
"render / is_dirty.");
|
||||
|
||||
ensure_editor();
|
||||
auto& s = editor_state();
|
||||
|
||||
section("language");
|
||||
{
|
||||
const char* labels[] = {"GLSL", "SQL", "Cpp", "Generic"};
|
||||
const fn::CodeLang langs[] = {
|
||||
fn::CodeLang::GLSL, fn::CodeLang::SQL, fn::CodeLang::Cpp, fn::CodeLang::Generic
|
||||
};
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
if (i > 0) ImGui::SameLine();
|
||||
bool active = (s.lang == langs[i]);
|
||||
if (active) ImGui::PushStyleColor(ImGuiCol_Button, colors::primary);
|
||||
if (ImGui::Button(labels[i])) apply_language(langs[i]);
|
||||
if (active) ImGui::PopStyleColor();
|
||||
}
|
||||
}
|
||||
|
||||
section("editor");
|
||||
{
|
||||
ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
float h = avail.y - 60.0f;
|
||||
if (h < 220.0f) h = 220.0f;
|
||||
fn::text_editor_render(s.ed, "##fn_text_editor_solo", ImVec2(-1, h));
|
||||
|
||||
if (fn::text_editor_is_dirty(s.ed)) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::warning);
|
||||
ImGui::TextUnformatted("(modified)");
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("clear dirty##te_solo")) fn::text_editor_clear_dirty(s.ed);
|
||||
} else {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim);
|
||||
ImGui::TextUnformatted("(clean)");
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
}
|
||||
|
||||
code_block(
|
||||
"auto* ed = fn::text_editor_create(fn::CodeLang::GLSL);\n"
|
||||
"fn::text_editor_set_text(ed, src);\n"
|
||||
"if (fn::text_editor_render(ed, \"##ed\", {600, 400}))\n"
|
||||
" on_changed(fn::text_editor_get_text(ed));"
|
||||
);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// file_watcher — watcher cross-platform no bloqueante
|
||||
// ===========================================================================
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* kWatchPath = "/tmp/fn_demo.glsl";
|
||||
|
||||
struct WatcherDemoState {
|
||||
fn::FileWatcher* fw = nullptr;
|
||||
bool active = false;
|
||||
std::string err;
|
||||
std::deque<std::string> events;
|
||||
};
|
||||
|
||||
WatcherDemoState& watcher_state() {
|
||||
static WatcherDemoState s;
|
||||
return s;
|
||||
}
|
||||
|
||||
void ensure_watcher() {
|
||||
auto& s = watcher_state();
|
||||
if (!s.fw) {
|
||||
s.fw = fn::file_watcher_create();
|
||||
s.active = fn::file_watcher_add(s.fw, kWatchPath);
|
||||
if (!s.active) s.err = fn::file_watcher_last_error(s.fw);
|
||||
}
|
||||
}
|
||||
|
||||
const char* kind_label(fn::FileEvent::Kind k) {
|
||||
switch (k) {
|
||||
case fn::FileEvent::Modified: return "MODIFIED";
|
||||
case fn::FileEvent::Created: return "CREATED";
|
||||
case fn::FileEvent::Deleted: return "DELETED";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
void poll_and_log() {
|
||||
auto& s = watcher_state();
|
||||
if (!s.fw) return;
|
||||
auto evs = fn::file_watcher_poll(s.fw);
|
||||
for (auto& e : evs) {
|
||||
char buf[512];
|
||||
std::snprintf(buf, sizeof(buf), "[%s] %s", kind_label(e.kind), e.path.c_str());
|
||||
s.events.push_back(buf);
|
||||
}
|
||||
while (s.events.size() > 200) s.events.pop_front();
|
||||
}
|
||||
|
||||
bool touch_demo_file(std::string& err_out) {
|
||||
FILE* f = std::fopen(kWatchPath, "a");
|
||||
if (!f) { err_out = std::strerror(errno); return false; }
|
||||
std::fprintf(f, "// touch %ld\n", (long)std::time(nullptr));
|
||||
std::fclose(f);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void demo_file_watcher() {
|
||||
using namespace fn_tokens;
|
||||
|
||||
demo_header("file_watcher", "v1.0.0",
|
||||
"Watcher de archivos cross-platform no bloqueante. Linux: inotify. Windows: "
|
||||
"ReadDirectoryChangesW. API: create / add / poll (drain) / destroy. Cap del "
|
||||
"buffer de eventos: 200.");
|
||||
|
||||
ensure_watcher();
|
||||
poll_and_log();
|
||||
|
||||
auto& s = watcher_state();
|
||||
|
||||
section("watcher state");
|
||||
ImGui::Text("path: %s", kWatchPath);
|
||||
ImGui::Text("active: %s", s.active ? "yes" : "no");
|
||||
if (!s.err.empty()) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::error);
|
||||
ImGui::TextWrapped("err: %s", s.err.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
section("trigger events");
|
||||
{
|
||||
if (ImGui::Button("touch (append timestamp)")) {
|
||||
std::string e;
|
||||
if (!touch_demo_file(e)) s.err = "touch failed: " + e;
|
||||
else s.err.clear();
|
||||
// Si el archivo no existia al inicio, reintenta el add.
|
||||
if (!s.active) {
|
||||
s.active = fn::file_watcher_add(s.fw, kWatchPath);
|
||||
if (!s.active) s.err = fn::file_watcher_last_error(s.fw);
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("clear events")) s.events.clear();
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("(o desde otro terminal: echo hi >> %s)", kWatchPath);
|
||||
}
|
||||
|
||||
section("event log");
|
||||
ImGui::Text("captured: %d", (int)s.events.size());
|
||||
ImGui::BeginChild("##fw_evlog", ImVec2(0, 0), ImGuiChildFlags_Borders);
|
||||
if (s.events.empty()) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim);
|
||||
ImGui::TextWrapped("Sin eventos. Pulsa touch o modifica el path desde otro terminal.");
|
||||
ImGui::PopStyleColor();
|
||||
} else {
|
||||
for (auto it = s.events.rbegin(); it != s.events.rend(); ++it) {
|
||||
ImGui::TextUnformatted(it->c_str());
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
code_block(
|
||||
"auto* fw = fn::file_watcher_create();\n"
|
||||
"fn::file_watcher_add(fw, \"/tmp/foo.glsl\");\n"
|
||||
"for (auto& e : fn::file_watcher_poll(fw)) {\n"
|
||||
" handle_event(e.path, e.kind);\n"
|
||||
"}"
|
||||
);
|
||||
}
|
||||
|
||||
} // namespace gallery
|
||||
Reference in New Issue
Block a user