732 lines
35 KiB
C++
732 lines
35 KiB
C++
#include "gui.h"
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <chrono>
|
|
|
|
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<std::pair<int,int>>& 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<int> 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<std::pair<int,int>>& 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<uint8_t> d((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
|
|
|
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<Widget>();
|
|
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<void(Widget&)>& 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<int(const Widget&)> 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<int(const Widget&, int, int)> 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<int(Widget&, int, int)> 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<float>(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<page>(); }
|
|
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;
|
|
}
|
|
} |