From 6fc470d37aa92e07d4afef7f10eb18b3bee23337 Mon Sep 17 00:00:00 2001 From: KoDer Date: Sun, 23 Aug 2026 18:17:50 +0000 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B=20=D0=B2=20=C2=AB?= =?UTF-8?q?/=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Terminus.ttf | Bin 0 -> 17392 bytes gui.cpp | 732 +++++++++++++++++++++++++++++++++++++++++++++++++++ gui.h | 211 +++++++++++++++ 3 files changed, 943 insertions(+) create mode 100644 Terminus.ttf create mode 100644 gui.cpp create mode 100644 gui.h diff --git a/Terminus.ttf b/Terminus.ttf new file mode 100644 index 0000000000000000000000000000000000000000..6bd6e7fb32fea47e1ba1686cf1399c87703c7dfa GIT binary patch literal 17392 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 +#include +#include +#include +#include + +namespace gui +{ + static const int ROW = 20; + static float ease(float t) { return t * t * (3 - 2 * t); } + static XRectangle intersect(const XRectangle& a, const XRectangle& b) { + int x1 = std::max((int)a.x, (int)b.x), y1 = std::max((int)a.y, (int)b.y); + int x2 = std::min((int)a.x + a.width, (int)b.x + b.width); + int y2 = std::min((int)a.y + a.height, (int)b.y + b.height); + if (x2 <= x1 || y2 <= y1) return {0, 0, 0, 0}; + return {(short)x1, (short)y1, (unsigned short)(x2 - x1), (unsigned short)(y2 - y1)}; + } + static void set_clip(Display* d, GC gc, const XRectangle& r) { + XRectangle rr = r; + XSetClipRectangles(d, gc, 0, 0, &rr, 1, Unsorted); + } + + EventBus& EventBus::get() { static EventBus inst; return inst; } + int EventBus::on(const std::string& e, Handler h) { + handlers[e].push_back({next_token, std::move(h)}); + return next_token++; + } + void EventBus::off(int t) { + for (auto& [name, vec] : handlers) + vec.erase(std::remove_if(vec.begin(), vec.end(), + [t](auto& p){ return p.first == t; }), vec.end()); + } + void EventBus::emit(const std::string& e, EventData d) { + auto it = handlers.find(e); + if (it == handlers.end()) return; + auto copy = it->second; // безопасно при off() внутри хендлера + for (auto& [t, h] : copy) h(d); + } + + // ================= RawPicture ================= + RawPicture::RawPicture(int w, int h, int bits) { create(w, h, bits); } + void RawPicture::create(int W, int H, int b) { + w = W; h = H; bits = (b == 8 || b == 16 || b == 24) ? b : 32; + buf.assign((size_t)w * h * bpp(), 0); + } + void RawPicture::setPixel(int x, int y, uint32_t c) { + if (x < 0 || y < 0 || x >= w || y >= h) return; + uint8_t r = (c >> 16) & 255, g = (c >> 8) & 255, b = c & 255; + uint8_t* p = &buf[(size_t)(y * w + x) * bpp()]; + switch (bits) { + case 8: p[0] = (uint8_t)((r * 299 + g * 587 + b * 114) / 1000); break; + case 16: { uint16_t v = (uint16_t)(((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3)); + p[0] = v & 255; p[1] = v >> 8; break; } + case 24: p[0] = r; p[1] = g; p[2] = b; break; + default: p[0] = r; p[1] = g; p[2] = b; p[3] = 255; break; + } + } + uint32_t RawPicture::getPixel(int x, int y) const { + if (x < 0 || y < 0 || x >= w || y >= h) return 0; + const uint8_t* p = &buf[(size_t)(y * w + x) * bpp()]; + switch (bits) { + case 8: return ((uint32_t)p[0] << 16) | ((uint32_t)p[0] << 8) | p[0]; + case 16: { uint16_t v = (uint16_t)(p[0] | (p[1] << 8)); + uint32_t r = (v >> 11) & 31, g = (v >> 5) & 63, b = v & 31; + return (r << 19 | r << 14) & 0xFF0000 | (g << 10 | g << 4) & 0xFF00 | (b << 3 | b >> 2); } + default: return ((uint32_t)p[0] << 16) | ((uint32_t)p[1] << 8) | p[2]; + } + } + void RawPicture::clear(uint32_t c) { for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) setPixel(x, y, c); } + void RawPicture::drawLine(int x0, int y0, int x1, int y1, uint32_t c) { + int dx = std::abs(x1 - x0), dy = -std::abs(y1 - y0); + int sx = x0 < x1 ? 1 : -1, sy = y0 < y1 ? 1 : -1, err = dx + dy; + while (true) { + setPixel(x0, y0, c); + if (x0 == x1 && y0 == y1) break; + int e2 = 2 * err; + if (e2 >= dy) { err += dy; x0 += sx; } + if (e2 <= dx) { err += dx; y0 += sy; } + } + } + void RawPicture::drawRect(int x, int y, int W, int H, uint32_t c) { + drawLine(x, y, x + W - 1, y, c); drawLine(x, y + H - 1, x + W - 1, y + H - 1, c); + drawLine(x, y, x, y + H - 1, c); drawLine(x + W - 1, y, x + W - 1, y + H - 1, c); + } + void RawPicture::fillRect(int x, int y, int W, int H, uint32_t c) { + for (int j = y; j < y + H; j++) for (int i = x; i < x + W; i++) setPixel(i, j, c); + } + void RawPicture::drawCircle(int cx, int cy, int r, uint32_t c) { + int x = r, y = 0, err = 0; + while (x >= y) { + setPixel(cx + x, cy + y, c); setPixel(cx + y, cy + x, c); + setPixel(cx - y, cy + x, c); setPixel(cx - x, cy + y, c); + setPixel(cx - x, cy - y, c); setPixel(cx - y, cy - x, c); + setPixel(cx + y, cy - x, c); setPixel(cx + x, cy - y, c); + y++; err += 1 + 2 * y; + if (2 * (err - x) + 1 > 0) { x--; err += 1 - 2 * x; } + } + } + void RawPicture::fillCircle(int cx, int cy, int r, uint32_t c) { + for (int dy = -r; dy <= r; dy++) { + int dx = (int)std::sqrt((double)r * r - dy * dy); + for (int x = cx - dx; x <= cx + dx; x++) setPixel(x, cy + dy, c); + } + } + void RawPicture::fillPolygon(const std::vector>& pts, uint32_t c) { + if (pts.size() < 3) return; + int miny = 1 << 30, maxy = -(1 << 30); + for (auto& p : pts) { miny = std::min(miny, p.second); maxy = std::max(maxy, p.second); } + for (int y = std::max(0, miny); y <= std::min(h - 1, maxy); y++) { + std::vector xs; + size_t n = pts.size(); + for (size_t i = 0, j = n - 1; i < n; j = i++) { + int y0 = pts[i].second, y1 = pts[j].second; + if ((y0 > y) != (y1 > y)) { + int x0 = pts[i].first, x1 = pts[j].first; + xs.push_back(x0 + (int)((double)(y - y0) / (y1 - y0) * (x1 - x0))); + } + } + std::sort(xs.begin(), xs.end()); + for (size_t i = 0; i + 1 < xs.size(); i += 2) + for (int x = xs[i]; x <= xs[i + 1]; x++) setPixel(x, y, c); + } + } + void RawPicture::drawPolygon(const std::vector>& pts, uint32_t c) { + for (size_t i = 0; i < pts.size(); i++) { + auto& a = pts[i]; auto& b = pts[(i + 1) % pts.size()]; + drawLine(a.first, a.second, b.first, b.second, c); + } + } + void RawPicture::drawTriangle(int x1,int y1,int x2,int y2,int x3,int y3,uint32_t c) { + drawLine(x1,y1,x2,y2,c); drawLine(x2,y2,x3,y3,c); drawLine(x3,y3,x1,y1,c); + } + void RawPicture::fillTriangle(int x1,int y1,int x2,int y2,int x3,int y3,uint32_t c) { + fillPolygon({{x1,y1},{x2,y2},{x3,y3}}, c); + } + void RawPicture::drawText(Display* dpy, int x, int y, const std::string& t, uint32_t c, int scale) { + if (!dpy || t.empty()) return; + int scr = DefaultScreen(dpy); + + static Font f = 0; + if (!f) { + const char* names[] = {"fixed", "-*-fixed-*-*-*-*-*-*-*-*-*-*-*", "6x13", "7x13", nullptr}; + for (int i = 0; names[i] && !f; i++) f = XLoadFont(dpy, names[i]); + } + if (!f) { + for (size_t i = 0; i < t.size(); i++) + fillRect(x + (int)i * 6 * scale, y, 5 * scale, 8 * scale, c); + return; + } + + XFontStruct* fs = XQueryFont(dpy, f); + if (!fs) return; // защита от NULL + + int cw = fs->max_bounds.width ? fs->max_bounds.width : 6; + int ch = fs->ascent + fs->descent; + if (cw <= 0 || ch <= 0) { XFreeFont(dpy, fs); return; } + + int tw = cw * (int)t.size(); + Pixmap pm = XCreatePixmap(dpy, RootWindow(dpy, scr), tw, ch, DefaultDepth(dpy, scr)); + GC g = XCreateGC(dpy, pm, 0, nullptr); + XSetForeground(dpy, g, 0); XFillRectangle(dpy, pm, g, 0, 0, tw, ch); + XSetForeground(dpy, g, 0xFFFFFF); XSetFont(dpy, g, f); + XDrawString(dpy, pm, g, 0, fs->ascent, t.c_str(), (int)t.size()); + + XImage* im = XGetImage(dpy, pm, 0, 0, tw, ch, AllPlanes, ZPixmap); + if (im) { + for (int py = 0; py < ch; py++) + for (int px = 0; px < tw; px++) + if (XGetPixel(im, px, py)) + for (int sy = 0; sy < scale; sy++) + for (int sx = 0; sx < scale; sx++) + setPixel(x + px * scale + sx, y + py * scale + sy, c); + XDestroyImage(im); + } + XFreeGC(dpy, g); XFreePixmap(dpy, pm); XFreeFont(dpy, fs); + } + void RawPicture::blit(const RawPicture& src, int dx, int dy) { + for (int y = 0; y < src.h; y++) + for (int x = 0; x < src.w; x++) + setPixel(dx + x, dy + y, src.getPixel(x, y)); + } + bool RawPicture::load(const std::string& path) { + std::ifstream file(path, std::ios::binary); + if (!file) return false; + std::vector d((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + + if (d.size() > 54 && d[0] == 'B' && d[1] == 'M') { // ---- BMP ---- + uint32_t off = *(uint32_t*)&d[10]; + int32_t W = *(int32_t*)&d[18], H = *(int32_t*)&d[22]; + uint16_t bppv = *(uint16_t*)&d[28]; + if (bppv != 24 && bppv != 32) return false; + create(W, std::abs(H), bits); + bool flip = H > 0; + int row_sz = ((W * (bppv / 8) + 3) / 4) * 4; + for (int y = 0; y < h; y++) { + int sy = flip ? (h - 1 - y) : y; + const uint8_t* row = &d[off + sy * row_sz]; + for (int x = 0; x < w; x++) { + const uint8_t* px = row + x * (bppv / 8); + setPixel(x, y, ((uint32_t)px[2] << 16) | ((uint32_t)px[1] << 8) | px[0]); + } + } + return true; + } + if (d.size() > 2 && d[0] == 'P' && d[1] == '6') { // ---- PPM P6 ---- + size_t pos = 2; int vals[3] = {0, 0, 0}, got = 0; + while (got < 3 && pos < d.size()) { + while (pos < d.size() && std::isspace(d[pos])) pos++; + if (d[pos] == '#') { while (pos < d.size() && d[pos] != '\n') pos++; continue; } + int v = 0; + while (pos < d.size() && std::isdigit(d[pos])) v = v * 10 + (d[pos++] - '0'); + if (v) vals[got++] = v; + } + pos++; + create(vals[0], vals[1], bits); + for (int y = 0; y < h && pos + 2 < d.size(); y++) + for (int x = 0; x < w; x++, pos += 3) + setPixel(x, y, ((uint32_t)d[pos] << 16) | ((uint32_t)d[pos + 1] << 8) | d[pos + 2]); + return true; + } + return false; + } + void RawPicture::present(Display* dpy, Window win, GC gc, int dx, int dy) const { + if (w <= 0 || h <= 0) return; + int scr = DefaultScreen(dpy); + char* data = (char*)malloc((size_t)w * h * 4); + uint32_t* px = (uint32_t*)data; + for (int y = 0; y < h; y++) + for (int x = 0; x < w; x++) px[y * w + x] = getPixel(x, y); + XImage* im = XCreateImage(dpy, DefaultVisual(dpy, scr), DefaultDepth(dpy, scr), + ZPixmap, 0, data, (unsigned)w, (unsigned)h, 32, w * 4); + XPutImage(dpy, win, gc, im, 0, 0, dx, dy, (unsigned)w, (unsigned)h); + XDestroyImage(im); + } + + Widget& combat::add_widget(WidgetData d) { + auto w = std::make_unique(); + w->data = std::move(d); + widgets.push_back(std::move(w)); + return *widgets.back(); + } + Widget& combat::Button(WidgetData a) { a.type = WidgetType::BUTTON; return add_widget(std::move(a)); } + Widget& combat::Label(WidgetData a) { a.type = WidgetType::LABEL; return add_widget(std::move(a)); } + Widget& combat::Input(WidgetData a) { a.type = WidgetType::INPUT; return add_widget(std::move(a)); } + Widget& combat::Checkbox(WidgetData a) { a.type = WidgetType::CHECKBOX; return add_widget(std::move(a)); } + Widget& combat::Radiobutton(WidgetData a) { a.type = WidgetType::RADIO; return add_widget(std::move(a)); } + Widget& combat::Slider(WidgetData a) { a.type = WidgetType::SLIDER; return add_widget(std::move(a)); } + Widget& combat::Progressbar(WidgetData a) { a.type = WidgetType::PROGRESS; return add_widget(std::move(a)); } + Widget& combat::Listbox(WidgetData a) { a.type = WidgetType::LISTBOX; return add_widget(std::move(a)); } + Widget& combat::Dropdown(WidgetData a) { a.type = WidgetType::DROPDOWN; return add_widget(std::move(a)); } + Widget& combat::TreeView(WidgetData a) { a.type = WidgetType::TREE_VIEW; return add_widget(std::move(a)); } + Widget& combat::TreeNode(WidgetData a) { a.type = WidgetType::TREE; return add_widget(std::move(a)); } + Widget& combat::ScrollView(WidgetData a) { a.type = WidgetType::SCROLL; return add_widget(std::move(a)); } + Widget& combat::RawPictureWidget(WidgetData a) { a.type = WidgetType::RAW_PIC; return add_widget(std::move(a)); } + Widget& combat::SpriteButton(WidgetData a){ a.type = WidgetType::SPRITE_BTN; return add_widget(std::move(a)); } + + void combat::visit(const std::function& fn) { + for (auto& w : widgets) { fn(*w); w->visit(fn); } + } + Widget* combat::find(const std::string& id) { + Widget* res = nullptr; + visit([&](Widget& w){ if (!res && w.data.id == id) res = &w; }); + return res; + } + void combat::tick(float dt) { for (auto& w : widgets) w->tick(dt); } + void combat::render_all(Display* d, Window win, GC gc, int ox, int oy, const XRectangle& clip, OverlayList* ov) const { + for (auto& w : widgets) w->draw(d, win, gc, ox, oy, clip, ov); + } + int combat::process_press(int mx, int my, window& w, page& p, int ox, int oy) { + for (auto it = widgets.rbegin(); it != widgets.rend(); ++it) { + int r = (*it)->handle_press(mx, my, w, p, ox, oy); + if (r) return r; + } + return 0; + } + bool combat::process_wheel(int delta, int mx, int my, int ox, int oy) { + for (auto it = widgets.rbegin(); it != widgets.rend(); ++it) + if ((*it)->handle_wheel(delta, mx, my, ox, oy)) return true; + return false; + } + + bool Widget::contains(int mx, int my, int ox, int oy) const { + int ax = data.x + ox, ay = data.y + oy; + return mx >= ax && mx <= ax + data.width && my >= ay && my <= ay + data.height; + } + + int Widget::content_height() const { + int ch = 0; + if (data.type == WidgetType::DROPDOWN) { + ch = (int)data.items.size() * ROW; + for (auto& c : widgets) ch += c->data.height + 2; + } else if (data.type == WidgetType::TREE_VIEW) { + // рекурсивный подсчёт строк дерева + std::function calc = [&](const Widget& n) -> int { + int s = ROW; + if (n.data.expanded) + for (auto& c : n.widgets) + s += (c->data.type == WidgetType::TREE) ? calc(*c) : c->data.height + 2; + return s; + }; + for (auto& c : widgets) ch += calc(*c); + } else { + ch = (int)data.items.size() * ROW; + for (auto& c : widgets) ch = std::max(ch, c->data.y + c->data.height); + } + return ch + 4; + } + + Widget* combat::find_at(int mx, int my) { + Widget* res = nullptr; + visit([&](Widget& w){ if (w.contains(mx, my, 0, 0)) res = &w; }); + return res; + } + + void Widget::tick(float dt) { + if (data.type == WidgetType::DROPDOWN) { + float target = open_target ? 1.f : 0.f; + anim += (target > anim ? dt * 10 : -dt * 10); + anim = std::clamp(anim, 0.f, 1.f); + } + if (data.type == WidgetType::PROGRESS) { + if (data.indeterminate) { + phase += dt * 0.9f; + data.value = std::fmod(phase, 1.3f) - 0.15f; + } else { + data.value += (data.target_value - data.value) * std::min(1.f, dt * 8); + if (std::abs(data.target_value - data.value) < 0.001f) data.value = data.target_value; + } + } + combat::tick(dt); + } + + void Widget::draw(Display* d, Window win, GC gc, int ox, int oy, const XRectangle& clip, OverlayList* ov) const { + int ax = data.x + ox, ay = data.y + oy; + XSetForeground(d, gc, data.bg_color); + XFillRectangle(d, win, gc, ax, ay, data.width, data.height); + + switch (data.type) { + case WidgetType::LABEL: + XSetForeground(d, gc, data.fg_color); + XDrawString(d, win, gc, ax, ay + 12, data.text.c_str(), (int)data.text.size()); + break; + case WidgetType::BUTTON: case WidgetType::INPUT: case WidgetType::SPRITE_BTN: { + if (data.picture) data.picture->present(d, win, gc, ax, ay); + XSetForeground(d, gc, data.frame_color); + XDrawRectangle(d, win, gc, ax, ay, data.width, data.height); + XSetForeground(d, gc, data.fg_color); + XDrawString(d, win, gc, ax + 5, ay + data.height / 2 + 5, data.text.c_str(), (int)data.text.size()); + if (data.type == WidgetType::INPUT) + XDrawLine(d, win, gc, ax + 5 + (int)data.text.size() * 7, ay + data.height - 6, + ax + 13 + (int)data.text.size() * 7, ay + data.height - 6); + break; + } + case WidgetType::CHECKBOX: { + int bs = data.height - 6; + XSetForeground(d, gc, data.frame_color); + XDrawRectangle(d, win, gc, ax + 3, ay + 3, bs, bs); + if (data.checked) { + XDrawLine(d, win, gc, ax + 5, ay + 5, ax + bs + 1, ay + bs + 1); + XDrawLine(d, win, gc, ax + bs + 1, ay + 5, ax + 5, ay + bs + 1); + } + XSetForeground(d, gc, data.fg_color); + XDrawString(d, win, gc, ax + bs + 10, ay + data.height / 2 + 5, data.text.c_str(), (int)data.text.size()); + break; + } + case WidgetType::RADIO: { + int r = (data.height - 6) / 2, cx = ax + r + 3, cy = ay + data.height / 2; + XSetForeground(d, gc, data.frame_color); + XDrawArc(d, win, gc, cx - r, cy - r, r * 2, r * 2, 0, 360 * 64); + if (data.checked) { int ir = r / 2; XFillArc(d, win, gc, cx - ir, cy - ir, ir * 2, ir * 2, 0, 360 * 64); } + XSetForeground(d, gc, data.fg_color); + XDrawString(d, win, gc, ax + r * 2 + 10, ay + data.height / 2 + 5, data.text.c_str(), (int)data.text.size()); + break; + } + case WidgetType::SLIDER: { + int ty = ay + data.height / 2 - 2; + XSetForeground(d, gc, 0x444444); + XFillRectangle(d, win, gc, ax, ty, data.width, 4); + XSetForeground(d, gc, data.frame_color); + XFillRectangle(d, win, gc, ax, ty, (int)(data.width * data.value), 4); + int kx = ax + (int)(data.value * (data.width - 10)); + XFillRectangle(d, win, gc, kx, ay + 2, 10, data.height - 4); + break; + } + case WidgetType::PROGRESS: { + XSetForeground(d, gc, data.frame_color); + XDrawRectangle(d, win, gc, ax, ay, data.width, data.height); + float v = std::clamp(data.value, 0.f, 1.f); + XFillRectangle(d, win, gc, ax + 1, ay + 1, (int)((data.width - 2) * v), data.height - 2); + XSetForeground(d, gc, data.fg_color); + std::string pct = std::to_string((int)(v * 100)) + "%"; + XDrawString(d, win, gc, ax + data.width / 2 - 10, ay + data.height / 2 + 5, pct.c_str(), (int)pct.size()); + break; + } + case WidgetType::LISTBOX: case WidgetType::SCROLL: { + XSetForeground(d, gc, data.frame_color); + XDrawRectangle(d, win, gc, ax, ay, data.width, data.height); + XRectangle vp = intersect(clip, {(short)(ax + 1), (short)(ay + 1), + (unsigned short)(data.width - 2), (unsigned short)(data.height - 2)}); + set_clip(d, gc, vp); + int cy = ay + 1 - data.scroll; + for (size_t i = 0; i < data.items.size(); i++) { + if ((int)i == data.selected_index) { + XSetForeground(d, gc, data.frame_color); + XFillRectangle(d, win, gc, ax + 1, cy, data.width - 2, ROW); + XSetForeground(d, gc, 0x000000); + } else XSetForeground(d, gc, data.fg_color); + XDrawString(d, win, gc, ax + 5, cy + 15, data.items[i].c_str(), (int)data.items[i].size()); + cy += ROW; + } + render_all(d, win, gc, ax + 1, ay + 1 - data.scroll, vp, ov); + set_clip(d, gc, clip); + // скроллбар + int chh = content_height(); + if (chh > data.height) { + int track = data.height - 4; + int thumb = std::max(15, track * data.height / chh); + int ty = ay + 2 + (int)((float)data.scroll / (chh - data.height) * (track - thumb)); + XSetForeground(d, gc, 0x555555); + XFillRectangle(d, win, gc, ax + data.width - 6, ty, 4, thumb); + } + break; + } + case WidgetType::TREE_VIEW: { + XSetForeground(d, gc, data.frame_color); + XDrawRectangle(d, win, gc, ax, ay, data.width, data.height); + XRectangle vp = intersect(clip, {(short)(ax + 1), (short)(ay + 1), + (unsigned short)(data.width - 2), (unsigned short)(data.height - 2)}); + set_clip(d, gc, vp); + std::function drawN = [&](const Widget& n, int depth, int y) -> int { + int nx = ax + 4 + depth * 14; + XSetForeground(d, gc, data.frame_color); + XDrawString(d, win, gc, nx, y + 15, n.data.expanded ? "[-]" : "[+]", 3); + XSetForeground(d, gc, data.fg_color); + XDrawString(d, win, gc, nx + 22, y + 15, n.data.text.c_str(), (int)n.data.text.size()); + y += ROW; + if (n.data.expanded) + for (auto& c : n.widgets) { + if (c->data.type == WidgetType::TREE) y = drawN(*c, depth + 1, y); + else { c->draw(d, win, gc, nx + 22, y - data.scroll + 0, vp, ov); y += c->data.height + 2; } + } + return y; + }; + int y = ay + 2 - data.scroll; + for (auto& c : widgets) y = drawN(*c, 0, y); + set_clip(d, gc, clip); + break; + } + case WidgetType::DROPDOWN: { + XSetForeground(d, gc, data.frame_color); + XDrawRectangle(d, win, gc, ax, ay, data.width, data.height); + XSetForeground(d, gc, data.fg_color); + std::string disp = (data.selected_index >= 0 && data.selected_index < (int)data.items.size()) + ? data.items[data.selected_index] : data.text; + XDrawString(d, win, gc, ax + 5, ay + data.height / 2 + 5, disp.c_str(), (int)disp.size()); + XDrawString(d, win, gc, ax + data.width - 14, ay + data.height / 2 + 5, "v", 1); + if (ov && anim > 0.01f) ov->push_back(this); // рисуется ПОСЛЕ всех, поверх + break; + } + case WidgetType::RAW_PIC: + if (data.picture) data.picture->present(d, win, gc, ax, ay); + break; + default: break; + } + } + + void Widget::draw_popup(Display* d, Window win, GC gc, const XRectangle& clip) const { + int ax = data.x, ay = data.y + data.height; + int total = content_height(); + int hgt = (int)(total * ease(anim)); + if (hgt <= 0) return; + XRectangle vp = intersect(clip, {(short)ax, (short)ay, (unsigned short)data.width, (unsigned short)hgt}); + set_clip(d, gc, vp); + XSetForeground(d, gc, 0x222222); + XFillRectangle(d, win, gc, ax, ay, data.width, hgt); + XSetForeground(d, gc, data.frame_color); + XDrawRectangle(d, win, gc, ax, ay, data.width, hgt); + int cy = ay; + for (size_t i = 0; i < data.items.size(); i++) { + if ((int)i == data.selected_index) { + XSetForeground(d, gc, data.frame_color); + XFillRectangle(d, win, gc, ax + 1, cy, data.width - 2, ROW); + XSetForeground(d, gc, 0x000000); + } else XSetForeground(d, gc, data.fg_color); + XDrawString(d, win, gc, ax + 5, cy + 15, data.items[i].c_str(), (int)data.items[i].size()); + cy += ROW; + } + for (auto& c : widgets) { c->draw(d, win, gc, ax + 1, cy, vp, nullptr); cy += c->data.height + 2; } + set_clip(d, gc, clip); + } + + int Widget::handle_press(int mx, int my, window& w, page& p, int ox, int oy) { + int ax = data.x + ox, ay = data.y + oy; + + if (data.type == WidgetType::DROPDOWN) { + if (open_target && mx >= ax && mx <= ax + data.width && + my >= ay + data.height && my <= ay + data.height + content_height()) { + int cy = ay + data.height; + for (size_t i = 0; i < data.items.size(); i++, cy += ROW) + if (my >= cy && my < cy + ROW) { + data.selected_index = (int)i; open_target = false; + EventBus::get().emit("dropdown.select", {data.id, data.items[i], 0, (int)i, false}); + if (data.on_change) data.on_change(); + return 1; + } + for (auto& c : widgets) { + int r = c->handle_press(mx, my, w, p, ax + 1, cy); + if (r) { open_target = false; return r; } + cy += c->data.height + 2; + } + return 1; + } + if (contains(mx, my, ox, oy)) { open_target = !open_target; return 1; } + if (open_target) { open_target = false; return 1; } + return 0; + } + + if (data.type == WidgetType::LISTBOX || data.type == WidgetType::SCROLL) { + if (!contains(mx, my, ox, oy)) return 0; + int r = process_press(mx, my, w, p, ax + 1, ay + 1 - data.scroll); + if (r) return r; + if (data.type == WidgetType::LISTBOX) { + int idx = (my - ay + data.scroll) / ROW; + if (idx >= 0 && idx < (int)data.items.size()) { + data.selected_index = idx; + EventBus::get().emit("list.select", {data.id, data.items[idx], 0, idx, false}); + if (data.on_change) data.on_change(); + } + } + return 1; + } + + if (data.type == WidgetType::TREE_VIEW) { + if (!contains(mx, my, ox, oy)) return 0; + std::function hitN = [&](Widget& n, int depth, int y) -> int { + int nx = ax + 4 + depth * 14; + if (my >= y && my < y + ROW && mx >= nx && mx < nx + 22 + 8 * 14) { + if (mx < nx + 20) { + n.data.expanded = !n.data.expanded; + EventBus::get().emit("tree.toggle", {n.data.id, n.data.text, 0, 0, n.data.expanded}); + return 1; + } + } + y += ROW; + if (n.data.expanded) + for (auto& c : n.widgets) { + if (c->data.type == WidgetType::TREE) { int r = hitN(*c, depth + 1, y); if (r) return r; + y += c->content_height(); } + else { int r = c->handle_press(mx, my, w, p, nx + 22, y); if (r) return r; y += c->data.height + 2; } + } + return 0; + }; + int y = ay + 2 - data.scroll; + for (auto& c : widgets) { int r = hitN(*c, 0, y); if (r) return r; y += c->content_height(); } + return 1; + } + + if (data.type == WidgetType::SLIDER && contains(mx, my, ox, oy)) { + data.value = std::clamp((float)(mx - ax) / data.width, 0.f, 1.f); + data.target_value = data.value; + cap_ox = ox; cap_oy = oy; + EventBus::get().emit("slider.change", {data.id, "", data.value, 0, false}); + if (data.on_change) data.on_change(); + return 2; // захват мыши для drag + } + + if (!contains(mx, my, ox, oy)) return 0; + switch (data.type) { + case WidgetType::BUTTON: case WidgetType::SPRITE_BTN: + EventBus::get().emit("button.click", {data.id, data.text, 0, 0, false}); + if (data.on_page_click) data.on_page_click(p); + if (data.on_window_click) data.on_window_click(w); + return 1; + case WidgetType::INPUT: w.set_focus(this); return 1; + case WidgetType::CHECKBOX: + data.checked = !data.checked; + EventBus::get().emit("checkbox.change", {data.id, data.text, 0, 0, data.checked}); + if (data.on_change) data.on_change(); + return 1; + case WidgetType::RADIO: + if (!data.checked) { + p.visit([&](Widget& x){ if (x.data.type == WidgetType::RADIO && x.data.id == data.id) x.data.checked = false; }); + data.checked = true; + EventBus::get().emit("radio.change", {data.id, data.text, 0, 0, true}); + if (data.on_change) data.on_change(); + } + return 1; + default: return 1; + } + } + + void Widget::handle_drag(int mx, int my) { + if (data.type == WidgetType::SLIDER) { + int ax = data.x + cap_ox; + data.value = std::clamp((float)(mx - ax) / data.width, 0.f, 1.f); + data.target_value = data.value; + EventBus::get().emit("slider.change", {data.id, "", data.value, 0, false}); + if (data.on_change) data.on_change(); + } + } + void Widget::handle_release() {} + + bool Widget::handle_wheel(int delta, int mx, int my, int ox, int oy) { + int ax = data.x + ox, ay = data.y + oy; + switch (data.type) { + case WidgetType::LISTBOX: case WidgetType::SCROLL: case WidgetType::TREE_VIEW: + if (!contains(mx, my, ox, oy)) return false; + if (process_wheel(delta, mx, my, ax + 1, ay + 1 - data.scroll)) return true; + data.scroll = std::clamp(data.scroll + delta * 20, 0, std::max(0, content_height() - data.height)); + return true; + case WidgetType::DROPDOWN: + if (open_target && contains(mx, my, ox, oy - data.height)) return true; + return contains(mx, my, ox, oy) ? true : false; + default: + if (contains(mx, my, ox, oy)) return process_wheel(delta, mx, my, ax, ay); + return false; + } + } + + window::window(std::string title) { + X11display = XOpenDisplay(NULL); + if (!X11display) { std::cerr << "Cannot open display\n"; return; } + int s = DefaultScreen(X11display); + X11window = XCreateSimpleWindow(X11display, RootWindow(X11display, s), 10, 10, 1200, 600, 1, + BlackPixel(X11display, s), 0x121212); + XStoreName(X11display, X11window, title.c_str()); + XSelectInput(X11display, X11window, ExposureMask | KeyPressMask | StructureNotifyMask | + ButtonPressMask | ButtonReleaseMask | ButtonMotionMask); + XMapWindow(X11display, X11window); + gc = XCreateGC(X11display, X11window, 0, NULL); + XFlush(X11display); + } + window::~window() { + if (gc) XFreeGC(X11display, gc); + if (X11window) XDestroyWindow(X11display, X11window); + if (X11display) XCloseDisplay(X11display); + } + + void window::update() { + if (!X11display) return; + static auto last = std::chrono::steady_clock::now(); + auto now = std::chrono::steady_clock::now(); + float dt = std::chrono::duration(now - last).count(); + last = now; + + while (XPending(X11display) > 0) { + XEvent ev; XNextEvent(X11display, &ev); + handle_event(ev); + } + for (auto& [n, pg] : pages) pg->tick(dt); + + XClearWindow(X11display, X11window); + draw_tabs(); + draw_active_page(); + XFlush(X11display); + } + + void window::handle_event(const XEvent& ev) { + if (ev.type == ButtonPress && ev.xbutton.button == Button1) { + int mx = ev.xbutton.x, my = ev.xbutton.y; + for (auto& tab : tab_buttons) + if (mx >= tab.x && mx <= tab.x + tab.width && my >= tab.y && my <= tab.y + tab.height) { + if (tab.on_window_click) tab.on_window_click(*this); + return; + } + if (!current_page_name.empty()) { + auto& pg = pages[current_page_name]; + int r = pg->process_press(mx, my, *this, *pg, 0, 0); + if (r == 2) captured = pg->find_at(mx, my); // виджет, захвативший мышь + } + } else if (ev.type == ButtonRelease) { + if (captured) { captured->handle_release(); captured = nullptr; } + } else if (ev.type == MotionNotify && captured) { + captured->handle_drag(ev.xmotion.x, ev.xmotion.y); + } else if (ev.type == ButtonPress && (ev.xbutton.button == Button4 || ev.xbutton.button == Button5)) { + int delta = ev.xbutton.button == Button4 ? -1 : 1; + if (!current_page_name.empty()) + pages[current_page_name]->process_wheel(delta, ev.xbutton.x, ev.xbutton.y, 0, 0); + } else if (ev.type == KeyPress && focused_input) { + char buf[16]; KeySym ks; + int len = XLookupString((XKeyEvent*)&ev, buf, sizeof(buf), &ks, nullptr); + if (ks == XK_BackSpace) { if (!focused_input->data.text.empty()) focused_input->data.text.pop_back(); } + else if (ks == XK_Return) { EventBus::get().emit("input.submit", {focused_input->data.id, focused_input->data.text}); focused_input = nullptr; } + else if (ks == XK_Escape) focused_input = nullptr; + else if (len > 0 && buf[0] >= 32) focused_input->data.text += buf[0]; + } + } + + void window::draw_tabs() const { + for (const auto& b : tab_buttons) { + Widget w; w.data = b; + XRectangle full{0, 0, 1200, 600}; + w.draw(X11display, X11window, gc, 0, 0, full, nullptr); + } + XSetForeground(X11display, gc, 0x333333); + XDrawLine(X11display, X11window, gc, 0, 80, 1200, 80); + } + void window::draw_active_page() const { + if (current_page_name.empty()) return; + auto it = pages.find(current_page_name); + if (it == pages.end()) return; + XRectangle full{0, 0, 1200, 600}; + OverlayList ov; + it->second->render_all(X11display, X11window, gc, 0, 0, full, &ov); + for (auto* w : ov) w->draw_popup(X11display, X11window, gc, full); // поверх всего + } + + void window::addTAB_BUTTON(WidgetData a) { + size_t i = tab_buttons.size(); + a.x = 10; a.y = 10 + (int)i * 35; a.width = 150; a.height = 30; + tab_buttons.push_back(std::move(a)); + } + void window::addPAGE(std::string n) { pages[n] = std::make_unique(); } + void window::configur_page(std::string n, callback_page fn, bool active) { + if (!pages.count(n)) addPAGE(n); + pages[n]->active = active; + if (active) current_page_name = n; + if (fn) fn(*pages[n]); + } + void window::openPAGE(std::string n) { + if (!pages.count(n)) return; + for (auto& [k, pg] : pages) pg->active = false; + pages[n]->active = true; + current_page_name = n; + EventBus::get().emit("page.open", {n, n}); + } + Widget* window::find(const std::string& id) { + for (auto& [n, pg] : pages) if (auto* w = pg->find(id)) return w; + return nullptr; + } +} \ No newline at end of file diff --git a/gui.h b/gui.h new file mode 100644 index 0000000..a0340f5 --- /dev/null +++ b/gui.h @@ -0,0 +1,211 @@ +#ifndef HOST_GUI_H +#define HOST_GUI_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace gui +{ + class window; + class page; + class RawPicture; + struct Widget; + + using callback_void = std::function; + using callback_page = std::function; + using callback_window = std::function; + + struct EventData { + std::string source_id; + std::string text; + float value = 0; + int index = -1; + bool flag = false; + }; + + class EventBus { + public: + using Handler = std::function; + static EventBus& get(); + int on(const std::string& event, Handler h); + void off(int token); + void emit(const std::string& event, EventData data = {}); + private: + int next_token = 1; + std::unordered_map>> handlers; + }; + + class RawPicture { + public: + RawPicture(int w = 0, int h = 0, int bits = 32); + void create(int w, int h, int bits); + int width() const { return w; } + int height() const { return h; } + int bitness() const { return bits; } + + void clear(uint32_t color = 0x000000); + void setPixel(int x, int y, uint32_t color); + uint32_t getPixel(int x, int y) const; + + void drawLine(int x0, int y0, int x1, int y1, uint32_t c); + void drawRect(int x, int y, int w, int h, uint32_t c); + void fillRect(int x, int y, int w, int h, uint32_t c); + void drawCircle(int cx, int cy, int r, uint32_t c); + void fillCircle(int cx, int cy, int r, uint32_t c); + void drawTriangle(int x1,int y1,int x2,int y2,int x3,int y3,uint32_t c); + void fillTriangle(int x1,int y1,int x2,int y2,int x3,int y3,uint32_t c); + void drawPolygon(const std::vector>& pts, uint32_t c); + void fillPolygon(const std::vector>& pts, uint32_t c); + void drawText(Display* dpy, int x, int y, const std::string& t, uint32_t c, int scale = 1); + void blit(const RawPicture& src, int dx, int dy); + bool load(const std::string& path); + void present(Display* dpy, Window win, GC gc, int dx, int dy) const; + + private: + int w = 0, h = 0, bits = 32; + std::vector buf; + int bpp() const { return bits / 8; } + }; + + // ================= Widgets ================= + enum class WidgetType { + BUTTON, LABEL, INPUT, CHECKBOX, RADIO, SLIDER, PROGRESS, + LISTBOX, DROPDOWN, TREE_VIEW, TREE, SCROLL, RAW_PIC, SPRITE_BTN + }; + + struct WidgetData { + WidgetType type = WidgetType::BUTTON; + std::string text; + std::string id; + unsigned long bg_color = 0x1A1A1A; + unsigned long fg_color = 0xFFFFFF; + unsigned long frame_color = 0x00FF00; + int x = 0; + int y = 0; + int width = 100; + int height = 30; + bool checked = false; + float value = 0.0f; + float target_value = 0.0f; + int selected_index = -1; + int scroll = 0; + bool expanded = false; + bool indeterminate = false; + std::vector items; + std::shared_ptr picture; + callback_void on_change; + callback_page on_page_click; + callback_window on_window_click; + }; + + using OverlayList = std::vector; + + class combat + { + friend struct Widget; + protected: + std::vector> widgets; + Widget& add_widget(WidgetData d); + public: + virtual ~combat() = default; + + Widget& Button(WidgetData a); + Widget& Label(WidgetData a); + Widget& Input(WidgetData a); + Widget& Checkbox(WidgetData a); + Widget& Radiobutton(WidgetData a); + Widget& Slider(WidgetData a); + Widget& Progressbar(WidgetData a); + Widget& Listbox(WidgetData a); + Widget& Dropdown(WidgetData a); + Widget& TreeView(WidgetData a); + Widget& TreeNode(WidgetData a); + Widget& ScrollView(WidgetData a); + Widget& RawPictureWidget(WidgetData a); + Widget& SpriteButton(WidgetData a); + + void visit(const std::function& fn); + Widget* find(const std::string& id); + Widget* find_at(int mx, int my); + + void tick(float dt); + void render_all(Display*, Window, GC, int ox, int oy, const XRectangle& clip, OverlayList* ov) const; + int process_press(int mx, int my, window& w, page& p, int ox, int oy); + bool process_wheel(int delta, int mx, int my, int ox, int oy); + }; + + struct Widget : combat + { + WidgetData data; + float anim = 0; + bool open_target = false; + float phase = 0; + int cap_ox = 0, cap_oy = 0; + bool thumb_drag = false; + int drag_my = 0, drag_scroll = 0; + + void setValue(float v) { data.target_value = v; if (data.type != WidgetType::PROGRESS) data.value = v; } + float getValue() const { return data.value; } + + void tick(float dt); + void draw(Display*, Window, GC, int ox, int oy, const XRectangle& clip, OverlayList* ov) const; + void draw_popup(Display*, Window, GC, const XRectangle& clip) const; + int handle_press(int mx, int my, window& w, page& p, int ox, int oy); + void handle_drag(int mx, int my); + void handle_release(); + bool handle_wheel(int delta, int mx, int my, int ox, int oy); + bool contains(int mx, int my, int ox, int oy) const; + int content_height() const; + }; + + class page : public combat { + public: + bool active = false; + int x = 0, y = 0; + int width = 1200, height = 600; + }; + + class window + { + private: + Display* X11display = nullptr; + Window X11window = 0; + GC gc = nullptr; + + std::vector tab_buttons; + std::unordered_map> pages; + std::string current_page_name; + + Widget* captured = nullptr; + Widget* focused_input = nullptr; + + void draw_tabs() const; + void draw_active_page() const; + void handle_event(const XEvent& ev); + + public: + explicit window(std::string title); + ~window(); + window(const window&) = delete; + window& operator=(const window&) = delete; + + Display* display() const { return X11display; } + void update(); + + void addTAB_BUTTON(WidgetData args); + void addPAGE(std::string name); + void configur_page(std::string name, callback_page fn, bool active = false); + void openPAGE(std::string name); + Widget* find(const std::string& id); + void set_focus(Widget* w) { focused_input = w; } + }; +} +#endif \ No newline at end of file