diff --git a/Terminus.ttf b/Terminus.ttf new file mode 100644 index 0000000..6bd6e7f Binary files /dev/null and b/Terminus.ttf differ diff --git a/gui.cpp b/gui.cpp new file mode 100644 index 0000000..2f53cc2 --- /dev/null +++ b/gui.cpp @@ -0,0 +1,732 @@ +#include "gui.h" +#include +#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