#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; } }