Initial commit: X11 GUI
This commit is contained in:
commit
975fba0ba8
9
LICENSE
Normal file
9
LICENSE
Normal file
@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 KoDer
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
284
README.md
Normal file
284
README.md
Normal file
@ -0,0 +1,284 @@
|
||||
# X11 GUI Library
|
||||
|
||||
[]()
|
||||
[]()
|
||||
[]()
|
||||
|
||||
Лёгкая GUI-библиотека для C++20 на X11: программный рендеринг, частичная перерисовка (dirty rectangles), TTF-шрифты (FreeType), две темы (WIN11 / TERMINAL), плавные анимации и встроенная отладка как в Android Studio.
|
||||
|
||||
---
|
||||
|
||||
## ✨ Возможности
|
||||
|
||||
- **Частичная перерисовка** — обновляются только изменённые области, а не весь экран
|
||||
- **Двойная буферизация** — без мерцания
|
||||
- **TTF/TTC шрифты** через FreeType с кешированием (быстрый текст)
|
||||
- **Две темы**: `WIN11` (скругления, синий акцент) и `TERMINAL` (квадратная, зелёная) + полная кастомизация
|
||||
- **Анимации**: hover, нажатие (push-down), галочки, dropdown, мигающий курсор
|
||||
- **EventBus** + колбэки на виджетах
|
||||
- **Отладка**: оверлей FPS, визуализация dirty-областей, маркеры кликов, границы виджетов (F1–F6)
|
||||
- **Только активная страница** рендерится — остальные не жгут CPU
|
||||
|
||||
---
|
||||
|
||||
## 🖼 Скриншоты
|
||||
|
||||
### Темы
|
||||
|
||||
| WIN11 | TERMINAL |
|
||||
|:---:|:---:|
|
||||
|  |  |
|
||||
|
||||
Переключение на лету — клавиша **F6**.
|
||||
|
||||
### Виджеты
|
||||
|
||||
| Controls (кнопки, ввод, чекбоксы, слайдер, прогресс) | Lists (список, dropdown, дерево, скролл) |
|
||||
|:---:|:---:|
|
||||
|  |  |
|
||||
|
||||
| Graphics (RawPicture: круги, полигоны, текст) |
|
||||
|:---:|
|
||||
|  |
|
||||
|
||||
### Отладка
|
||||
|
||||
| Debug overlay (F1) — FPS, виджеты, partial updates, лог событий | Dirty rectangles (F2) — подсветка перерисованных областей |
|
||||
|:---:|:---:|
|
||||
|  |  |
|
||||
|
||||
| Маркеры кликов (F3) | Границы виджетов (F4) |
|
||||
|:---:|:---:|
|
||||
|  |  |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Быстрый старт
|
||||
|
||||
```cpp
|
||||
#include "gui.h"
|
||||
|
||||
int main() {
|
||||
gui::set_theme(gui::ThemeStyle::WIN11);
|
||||
gui::window win("My App");
|
||||
win.set_font("SGr-Iosevka-Regular.ttc"); // любой TTF/TTC рядом с бинарником
|
||||
win.set_default_icon();
|
||||
|
||||
win.configur_page("main", [](gui::page& p) {
|
||||
gui::WidgetData b;
|
||||
b.id = "btn"; b.text = "Hello";
|
||||
b.x = 100; b.y = 100; b.width = 300; b.height = 70;
|
||||
b.primary = true; // акцентная кнопка
|
||||
p.Button(b);
|
||||
}, true);
|
||||
|
||||
gui::EventBus::get().on("button.click", [](const gui::EventData& e) {
|
||||
std::cout << "clicked: " << e.source_id << "\n";
|
||||
});
|
||||
|
||||
while (win.running()) win.update();
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
Или просто запусти демо со всеми виджетами:
|
||||
|
||||
```cpp
|
||||
#include "gui.h"
|
||||
int main() { gui::run_demo(); }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠 Сборка
|
||||
|
||||
### Зависимости
|
||||
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt install libx11-dev libfreetype-dev cmake g++
|
||||
|
||||
# Fedora
|
||||
sudo dnf install libX11-devel freetype-devel cmake gcc-c++
|
||||
|
||||
# Arch
|
||||
sudo pacman -S libx11 freetype2 cmake
|
||||
```
|
||||
|
||||
### CMake
|
||||
|
||||
```cmake
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
project(host)
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
find_package(X11 REQUIRED)
|
||||
find_package(Freetype REQUIRED)
|
||||
|
||||
add_executable(host
|
||||
main.cpp
|
||||
libs/gui/core/event_bus.cpp
|
||||
libs/gui/core/raw_picture.cpp
|
||||
libs/gui/core/font_manager.cpp
|
||||
libs/gui/core/theme.cpp
|
||||
libs/gui/widgets/widget.cpp
|
||||
libs/gui/widgets/draw_basic.cpp
|
||||
libs/gui/widgets/draw_toggles.cpp
|
||||
libs/gui/widgets/draw_lists.cpp
|
||||
libs/gui/window.cpp
|
||||
libs/gui/demo.cpp
|
||||
)
|
||||
target_include_directories(host PRIVATE libs/gui ${X11_INCLUDE_DIR})
|
||||
target_link_libraries(host PRIVATE ${X11_LIBRARIES} Freetype::Freetype)
|
||||
```
|
||||
|
||||
```bash
|
||||
mkdir build && cd build
|
||||
cmake ..
|
||||
make -j$(nproc)
|
||||
./host
|
||||
```
|
||||
|
||||
> Положи `SGr-Iosevka-Regular.ttc` (или любой TTF) рядом с бинарником. Если шрифт не найден — используется фолбэк на X11 core font.
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Виджеты
|
||||
|
||||
| Метод | Тип | Описание |
|
||||
|-------|-----|----------|
|
||||
| `Button(d)` | BUTTON | Кнопка (`primary = true` — акцентная) |
|
||||
| `Label(d)` | LABEL | Текстовая метка |
|
||||
| `Input(d)` | INPUT | Поле ввода: клик — фокус, нижняя линия, мигающий курсор, Enter — `input.submit` |
|
||||
| `Checkbox(d)` | CHECKBOX | Флажок с анимированной галочкой |
|
||||
| `Radiobutton(d)` | RADIO | Радио-кнопка (группа по `id`) |
|
||||
| `Slider(d)` | SLIDER | Ползунок 0..1, перетаскивание |
|
||||
| `Progressbar(d)` | PROGRESS | Прогресс (+ `indeterminate` — бегущий) |
|
||||
| `Listbox(d)` | LISTBOX | Список с прокруткой и выделением |
|
||||
| `Dropdown(d)` | DROPDOWN | Выпадающий список с анимацией |
|
||||
| `TreeView(d)` + `TreeNode(d)` | TREE_VIEW / TREE | Дерево с раскрытием `[+]/[-]` |
|
||||
| `ScrollView(d)` | SCROLL | Контейнер с прокруткой |
|
||||
| `RawPictureWidget(d)` | RAW_PIC | Картинка из `RawPicture` |
|
||||
| `SpriteButton(d)` | SPRITE_BTN | Кликабельный спрайт |
|
||||
|
||||
### Основные поля `WidgetData`
|
||||
|
||||
```cpp
|
||||
std::string id, text; // идентификатор и подпись
|
||||
int x, y, width, height; // геометрия (масштаб x2)
|
||||
unsigned long bg_color, fg_color, frame_color; // 0 = из темы, иначе override
|
||||
int font_size; // 0 = системный, иначе свой
|
||||
bool primary; // акцентная кнопка
|
||||
bool checked; // checkbox / radio
|
||||
float value, target_value; // slider / progress (0..1)
|
||||
std::vector<std::string> items;// listbox / dropdown
|
||||
std::shared_ptr<RawPicture> picture;
|
||||
// колбэки:
|
||||
on_change / on_page_click / on_window_click
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐞 Отладка
|
||||
|
||||
### Горячие клавиши
|
||||
|
||||
| Клавиша | Действие |
|
||||
|---------|----------|
|
||||
| **F1** | Debug overlay: FPS, кол-во виджетов, счётчик partial updates, координаты мыши, лог последних событий |
|
||||
| **F2** | Визуализация dirty rectangles — мигание перерисованных областей (как «Show screen updates» в Android) |
|
||||
| **F3** | Маркеры кликов — красный крестик в точке нажатия |
|
||||
| **F4** | Границы всех виджетов (magenta) |
|
||||
| **F5** | Логирование событий в консоль |
|
||||
| **F6** | Переключение темы WIN11 ↔ TERMINAL |
|
||||
|
||||
### Как читать оверлей (F1)
|
||||
|
||||
- **FPS** — реальная частота перерисовок (в простое ~0, при анимациях до `max_fps`)
|
||||
- **Widgets** — виджетов на активной странице
|
||||
- **Partial updates** — сколько dirty-областей перерисовано всего. Растёт медленно = оптимизация работает
|
||||
- **Mouse** — координаты курсора
|
||||
- **EVENT LOG** — последние 5 событий шины
|
||||
|
||||
### Что показывает F2
|
||||
|
||||
Каждая перерисованная область на 0.2 сек подсвечивается XOR-цветом. Наведи на кнопку — мигнёт только она. Двинь слайдер — мигнёт только слайдер. Если мигает весь экран — значит ты сам позвал `mark_all_dirty()`.
|
||||
|
||||
### Программное управление
|
||||
|
||||
```cpp
|
||||
win.debug().show_debug_overlay = true; // F1
|
||||
win.debug().show_dirty_rects = true; // F2
|
||||
win.debug().show_click_markers = true; // F3
|
||||
win.debug().show_widget_bounds = true; // F4
|
||||
win.debug().log_to_console = true; // F5
|
||||
win.debug().max_log_size = 30;
|
||||
|
||||
win.mark_dirty(x, y, w, h); // пометить область как грязную
|
||||
win.mark_all_dirty(); // перерисовать всё
|
||||
win.set_max_fps(144); // лимит кадров
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
3. Что снять (имена файлов = ссылки в README):
|
||||
|
||||
| Файл | Что показать |
|
||||
|------|--------------|
|
||||
| `theme_win11.png` | демо в теме WIN11 |
|
||||
| `theme_terminal.png` | нажми F6 → тема TERMINAL |
|
||||
| `widgets_controls.png` | страница controls |
|
||||
| `widgets_lists.png` | страница lists |
|
||||
| `widgets_graphics.png` | страница graphics |
|
||||
| `debug_overlay.png` | нажми F1 |
|
||||
| `debug_dirty.png` | F2 + покликай кнопки |
|
||||
| `debug_clicks.png` | F3 + покликай |
|
||||
| `debug_bounds.png` | F4 |
|
||||
|
||||
4. Закоммить папку `docs/screenshots/` — и картинки появятся на Gitea.
|
||||
|
||||
---
|
||||
|
||||
## 📁 Структура проекта
|
||||
|
||||
```
|
||||
/
|
||||
├── README.md
|
||||
├── docs/
|
||||
│ ├── index.html ← интерактивная документация
|
||||
│ └── screenshots/ ← скриншоты для README
|
||||
└── gui/
|
||||
├── gui.h ← umbrella-заголовок
|
||||
├── demo.h / demo.cpp ← run_demo()
|
||||
├── window.h / window.cpp ← окно, dirty-рендер, оверлей, иконка
|
||||
├── core/
|
||||
│ ├── common.h ← EventData, DebugInfo, DirtyRegion, easing
|
||||
│ ├── event_bus.h/.cpp
|
||||
│ ├── raw_picture.h/.cpp ← пиксельный буфер + примитивы + BMP/PPM
|
||||
│ ├── font_manager.h/.cpp ← TTF + кеш текста
|
||||
│ └── theme.h/.cpp ← WIN11 / TERMINAL
|
||||
└── widgets/
|
||||
├── widget.h / widget.cpp ← WidgetData, Widget, combat, page
|
||||
├── draw_basic.cpp ← Button, Label, Input, Sprite
|
||||
├── draw_toggles.cpp ← Checkbox, Radio, Slider, Progress
|
||||
└── draw_lists.cpp ← Listbox, Dropdown, Tree, Scroll
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Документация
|
||||
|
||||
Полная интерактивная документация (9 страниц: старт, окно, виджеты, темы, свои виджеты, события, анимации, отладка, API):
|
||||
|
||||
👉 **[docs/index.html](docs/index.html)** — открой в браузере
|
||||
|
||||
---
|
||||
|
||||
## 📄 Лицензия
|
||||
|
||||
MIT.
|
||||
|
||||
## 👤 Автор
|
||||
|
||||
**KoDer** — [git.bipfr.ru/KoDer/gui](https://git.bipfr.ru/KoDer)
|
||||
522
docs.html
Normal file
522
docs.html
Normal file
@ -0,0 +1,522 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>GUI Library Documentation</title>
|
||||
<!-- Подключаем шрифты и иконки -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css">
|
||||
<style>
|
||||
:root {
|
||||
--bg-primary: #0f172a;
|
||||
--bg-secondary: #1e293b;
|
||||
--bg-tertiary: #334155;
|
||||
--text-primary: #f1f5f9;
|
||||
--text-secondary: #94a3b8;
|
||||
--accent: #38bdf8;
|
||||
--accent-hover: #0ea5e9;
|
||||
--success: #22c55e;
|
||||
--border: #334155;
|
||||
--code-bg: #0d1117;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Скроллбар */
|
||||
::-webkit-scrollbar { width: 8px; height: 8px; }
|
||||
::-webkit-scrollbar-track { background: var(--bg-primary); }
|
||||
::-webkit-scrollbar-thumb { background: var(--bg-tertiary); border-radius: 4px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: var(--accent); }
|
||||
|
||||
/* Боковая панель */
|
||||
.sidebar {
|
||||
width: 280px;
|
||||
background-color: var(--bg-secondary);
|
||||
border-right: 1px solid var(--border);
|
||||
position: fixed;
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
padding: 2rem 1.5rem;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--accent);
|
||||
margin-bottom: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
color: var(--text-primary);
|
||||
font-family: 'Inter', sans-serif;
|
||||
margin-bottom: 1.5rem;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.search-box:focus { border-color: var(--accent); }
|
||||
|
||||
.nav-group { margin-bottom: 1.5rem; }
|
||||
.nav-title {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: block;
|
||||
padding: 0.5rem 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.nav-link:hover, .nav-link.active {
|
||||
background-color: var(--bg-tertiary);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* Основной контент */
|
||||
.main-content {
|
||||
margin-left: 280px;
|
||||
flex: 1;
|
||||
padding: 3rem 4rem;
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
h1 { font-size: 2.5rem; margin-bottom: 1rem; color: var(--text-primary); }
|
||||
h2 { font-size: 1.8rem; margin-top: 3rem; margin-bottom: 1rem; color: var(--accent); border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; }
|
||||
h3 { font-size: 1.3rem; margin-top: 2rem; margin-bottom: 0.75rem; color: var(--text-primary); }
|
||||
p { margin-bottom: 1rem; color: var(--text-secondary); }
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--accent);
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
/* Блоки кода */
|
||||
.code-wrapper {
|
||||
position: relative;
|
||||
margin: 1.5rem 0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.code-header {
|
||||
background: var(--bg-secondary);
|
||||
padding: 0.5rem 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.copy-btn {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-secondary);
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.copy-btn:hover { background: var(--bg-tertiary); color: var(--text-primary); }
|
||||
.copy-btn.copied { border-color: var(--success); color: var(--success); }
|
||||
|
||||
pre { margin: 0 !important; border-radius: 0 !important; background: var(--code-bg) !important; }
|
||||
code { font-family: 'JetBrains Mono', monospace !important; font-size: 0.9rem !important; }
|
||||
|
||||
/* Таблицы */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1.5rem 0;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
th, td {
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
th { background: var(--bg-tertiary); color: var(--text-primary); font-weight: 600; }
|
||||
td { color: var(--text-secondary); }
|
||||
td code { background: var(--bg-primary); padding: 0.2rem 0.4rem; border-radius: 4px; color: var(--accent); }
|
||||
|
||||
/* Аккордеон для API */
|
||||
details {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
summary {
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
list-style: none;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
summary::-webkit-details-marker { display: none; }
|
||||
summary::after { content: '+'; font-size: 1.5rem; color: var(--accent); }
|
||||
details[open] summary::after { content: '−'; }
|
||||
details[open] summary { border-bottom: 1px solid var(--border); }
|
||||
.details-content { padding: 1.5rem; }
|
||||
|
||||
.highlight { color: var(--accent); font-weight: 600; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sidebar { transform: translateX(-100%); transition: transform 0.3s; }
|
||||
.sidebar.open { transform: translateX(0); }
|
||||
.main-content { margin-left: 0; padding: 2rem; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Боковая навигация -->
|
||||
<nav class="sidebar">
|
||||
<div class="logo">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/></svg>
|
||||
X11 GUI Lib
|
||||
</div>
|
||||
<input type="text" class="search-box" placeholder="Поиск по документации..." id="searchInput">
|
||||
|
||||
<div class="nav-group">
|
||||
<div class="nav-title">Начало работы</div>
|
||||
<a href="#intro" class="nav-link">Введение</a>
|
||||
<a href="#quickstart" class="nav-link">Быстрый старт</a>
|
||||
<a href="#architecture" class="nav-link">Архитектура</a>
|
||||
</div>
|
||||
<div class="nav-group">
|
||||
<div class="nav-title">Компоненты</div>
|
||||
<a href="#eventbus" class="nav-link">EventBus</a>
|
||||
<a href="#rawpicture" class="nav-link">RawPicture</a>
|
||||
<a href="#widgets" class="nav-link">Виджеты</a>
|
||||
</div>
|
||||
<div class="nav-group">
|
||||
<div class="nav-title">API Справочник</div>
|
||||
<a href="#api-window" class="nav-link">gui::window</a>
|
||||
<a href="#api-page" class="nav-link">gui::page</a>
|
||||
<a href="#api-combat" class="nav-link">gui::combat</a>
|
||||
<a href="#api-widget" class="nav-link">gui::Widget</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Основной контент -->
|
||||
<main class="main-content">
|
||||
<section id="intro">
|
||||
<h1>Документация GUI Библиотеки</h1>
|
||||
<p>Легковесная, кроссплатформенная (X11) библиотека для создания графических интерфейсов на C++. Поддерживает программный рендеринг, анимации, иерархию виджетов и событийно-ориентированную архитектуру.</p>
|
||||
</section>
|
||||
|
||||
<section id="quickstart">
|
||||
<h2>🚀 Быстрый старт</h2>
|
||||
<p>Минимальный пример создания окна с кнопкой и обработкой события.</p>
|
||||
<div class="code-wrapper">
|
||||
<div class="code-header">
|
||||
<span>main.cpp</span>
|
||||
<button class="copy-btn" onclick="copyCode(this)">Копировать</button>
|
||||
</div>
|
||||
<pre><code class="language-cpp">#include "gui.h"
|
||||
|
||||
int main() {
|
||||
// 1. Создаем окно
|
||||
gui::window win("Мое Приложение");
|
||||
|
||||
// 2. Добавляем страницу
|
||||
win.addPAGE("main");
|
||||
|
||||
// 3. Настраиваем страницу виджетами
|
||||
win.configur_page("main", [](gui::page& p) {
|
||||
// Создаем кнопку
|
||||
gui::WidgetData btnData;
|
||||
btnData.id = "my_button";
|
||||
btnData.text = "Нажми меня!";
|
||||
btnData.x = 50; btnData.y = 50;
|
||||
btnData.width = 150; btnData.height = 40;
|
||||
btnData.bg_color = 0x2563EB; // Синий
|
||||
btnData.fg_color = 0xFFFFFF; // Белый текст
|
||||
|
||||
p.Button(btnData);
|
||||
}, true); // true = сделать активной сразу
|
||||
|
||||
// 4. Подписываемся на событие
|
||||
gui::EventBus::get().on("button.click", [](const gui::EventData& e) {
|
||||
if (e.source_id == "my_button") {
|
||||
std::cout << "Кнопка нажата!" << std::endl;
|
||||
}
|
||||
});
|
||||
|
||||
// 5. Главный цикл
|
||||
while (true) {
|
||||
win.update(); // Обработка событий X11 и отрисовка
|
||||
// Добавьте std::this_thread::sleep_for для экономии CPU
|
||||
}
|
||||
|
||||
return 0;
|
||||
}</code></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="architecture">
|
||||
<h2>🏛 Архитектура</h2>
|
||||
<p>Библиотека построена на строгой иерархии:</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Уровень</th><th>Класс</th><th>Описание</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><span class="badge">1</span></td><td><code>gui::window</code></td><td>Управляет соединением с X11, главным циклом (`update`) и вкладками.</td></tr>
|
||||
<tr><td><span class="badge">2</span></td><td><code>gui::page</code></td><td>Представляет собой вкладку или экран. Наследуется от <code>combat</code>.</td></tr>
|
||||
<tr><td><span class="badge">3</span></td><td><code>gui::combat</code></td><td>Базовый контейнер. Хранит коллекцию виджетов и предоставляет фабричные методы для их создания.</td></tr>
|
||||
<tr><td><span class="badge">4</span></td><td><code>gui::Widget</code></td><td>Конкретный элемент интерфейса (кнопка, ввод, слайдер). Также наследуется от <code>combat</code>, позволяя создавать вложенные структуры (например, деревья).</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section id="eventbus">
|
||||
<h2>📡 EventBus</h2>
|
||||
<p>Глобальная шина событий для слабой связи между компонентами. Реализована как Singleton.</p>
|
||||
<div class="code-wrapper">
|
||||
<div class="code-header"><span>Использование EventBus</span><button class="copy-btn" onclick="copyCode(this)">Копировать</button></div>
|
||||
<pre><code class="language-cpp">// Подписка на событие
|
||||
int token = gui::EventBus::get().on("slider.change", [](const gui::EventData& e) {
|
||||
std::cout << "Widget: " << e.source_id << ", Value: " << e.value << std::endl;
|
||||
});
|
||||
|
||||
// Генерация события (обычно делается внутри библиотеки, но можно и вручную)
|
||||
gui::EventBus::get().emit("custom.event", {"widget_id", "some text", 0.5f, 0, true});
|
||||
|
||||
// Отписка
|
||||
gui::EventBus::get().off(token);</code></pre>
|
||||
</div>
|
||||
<p>Структура <code>EventData</code> содержит: <code>source_id</code> (ID виджета), <code>text</code>, <code>value</code> (для слайдеров/прогресса), <code>index</code> (для списков), <code>flag</code> (для чекбоксов).</p>
|
||||
</section>
|
||||
|
||||
<section id="rawpicture">
|
||||
<h2>🎨 RawPicture</h2>
|
||||
<p>Класс для программного рендеринга в буфер. Поддерживает 8, 16, 24 и 32-битные форматы.</p>
|
||||
<ul>
|
||||
<li><strong>Примитивы:</strong> <code>drawLine</code>, <code>fillRect</code>, <code>drawCircle</code>, <code>fillPolygon</code>, <code>drawText</code>.</li>
|
||||
<li><strong>Загрузка:</strong> <code>load("image.bmp")</code> или <code>load("image.ppm")</code>.</li>
|
||||
<li><strong>Отображение:</strong> <code>present(display, window, gc, x, y)</code> для вывода на экран X11.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section id="widgets">
|
||||
<h2>🧩 Доступные виджеты</h2>
|
||||
<p>Все виджеты создаются через методы класса <code>combat</code> (или <code>page</code>) путем передачи структуры <code>WidgetData</code>.</p>
|
||||
<table>
|
||||
<thead><tr><th>Метод</th><th>Тип</th><th>Описание</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>Button()</code></td><td><code>BUTTON</code></td><td>Стандартная кнопка. Поддерживает <code>on_page_click</code> и <code>on_window_click</code>.</td></tr>
|
||||
<tr><td><code>Label()</code></td><td><code>LABEL</code></td><td>Текстовая метка.</td></tr>
|
||||
<tr><td><code>Input()</code></td><td><code>INPUT</code></td><td>Поле ввода текста. Перехватывает фокус клавиатуры.</td></tr>
|
||||
<tr><td><code>Checkbox()</code></td><td><code>CHECKBOX</code></td><td>Флажок. Состояние в <code>data.checked</code>.</td></tr>
|
||||
<tr><td><code>Radiobutton()</code></td><td><code>RADIO</code></td><td>Радиокнопка. Автоматически снимает выделение с других радио-кнопок с тем же <code>id</code> на странице.</td></tr>
|
||||
<tr><td><code>Slider()</code></td><td><code>SLIDER</code></td><td>Ползунок. Значение 0.0 - 1.0 в <code>data.value</code>. Поддерживает перетаскивание.</td></tr>
|
||||
<tr><td><code>Progressbar()</code></td><td><code>PROGRESS</code></td><td>Индикатор прогресса. Поддерживает анимацию <code>indeterminate</code>.</td></tr>
|
||||
<tr><td><code>Listbox()</code></td><td><code>LISTBOX</code></td><td>Список элементов. Элементы задаются через <code>data.items</code>.</td></tr>
|
||||
<tr><td><code>Dropdown()</code></td><td><code>DROPDOWN</code></td><td>Выпадающий список с анимацией открытия.</td></tr>
|
||||
<tr><td><code>TreeView()</code></td><td><code>TREE_VIEW</code></td><td>Контейнер для древовидной структуры. Использует <code>TreeNode</code> как детей.</td></tr>
|
||||
<tr><td><code>ScrollView()</code></td><td><code>SCROLL</code></td><td>Область с прокруткой содержимого.</td></tr>
|
||||
<tr><td><code>RawPictureWidget()</code></td><td><code>RAW_PIC</code></td><td>Отображает объект <code>RawPicture</code> из <code>data.picture</code>.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section id="api-window">
|
||||
<h2>📚 API Справочник: gui::window</h2>
|
||||
<details>
|
||||
<summary><code>window(std::string title)</code></summary>
|
||||
<div class="details-content">
|
||||
<p>Конструктор. Инициализирует соединение с X11, создает окно заданного размера (по умолчанию 1200x600) и настраивает маску событий.</p>
|
||||
</div>
|
||||
</details>
|
||||
<details>
|
||||
<summary><code>void update()</code></summary>
|
||||
<div class="details-content">
|
||||
<p>Главный метод цикла. Обрабатывает все накопленные события X11, обновляет анимации виджетов (<code>tick</code>), очищает окно и перерисовывает вкладки и активную страницу. <strong>Должен вызываться постоянно.</strong></p>
|
||||
</div>
|
||||
</details>
|
||||
<details>
|
||||
<summary><code>void addPAGE(std::string name)</code> / <code>void configur_page(...)</code></summary>
|
||||
<div class="details-content">
|
||||
<p><code>addPAGE</code> создает пустую страницу. <code>configur_page</code> создает (если нет), применяет лямбда-функцию для наполнения виджетами и может сделать её активной (<code>active = true</code>).</p>
|
||||
</div>
|
||||
</details>
|
||||
<details>
|
||||
<summary><code>void openPAGE(std::string name)</code></summary>
|
||||
<div class="details-content">
|
||||
<p>Делает указанную страницу активной, деактивируя остальные. Генерирует событие <code>"page.open"</code>.</p>
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<section id="api-combat">
|
||||
<h2>📚 API Справочник: gui::combat</h2>
|
||||
<p>Базовый класс для контейнеров. Все методы создания виджетов возвращают ссылку на созданный <code>Widget</code>, что позволяет сохранять его для последующего изменения.</p>
|
||||
<details>
|
||||
<summary>Фабричные методы (<code>Button</code>, <code>Label</code>, <code>Input</code> и т.д.)</summary>
|
||||
<div class="details-content">
|
||||
<p>Принимают объект <code>WidgetData</code>. Автоматически устанавливают соответствующий <code>WidgetType</code>, добавляют виджет во внутренний вектор и возвращают на него ссылку.</p>
|
||||
</div>
|
||||
</details>
|
||||
<details>
|
||||
<summary><code>Widget* find(const std::string& id)</code></summary>
|
||||
<div class="details-content">
|
||||
<p>Рекурсивный поиск виджета по его уникальному идентификатору <code>id</code> внутри контейнера и всех его дочерних элементов.</p>
|
||||
</div>
|
||||
</details>
|
||||
<details>
|
||||
<summary><code>void visit(const std::function<void(Widget&)>& fn)</code></summary>
|
||||
<div class="details-content">
|
||||
<p>Применяет переданную лямбда-функцию ко всем виджетам в иерархии. Удобно для массовой модификации (например, отключения всех кнопок).</p>
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<section id="api-widget">
|
||||
<h2>📚 API Справочник: gui::Widget</h2>
|
||||
<details>
|
||||
<summary><code>void setValue(float v)</code> / <code>float getValue()</code></summary>
|
||||
<div class="details-content">
|
||||
<p>Устанавливает целевое значение. Для прогресс-баров и слайдеров это запускает плавную анимацию изменения <code>data.value</code> к <code>data.target_value</code> внутри метода <code>tick()</code>.</p>
|
||||
</div>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Анимация и состояние</summary>
|
||||
<div class="details-content">
|
||||
<p>Виджет содержит поля <code>anim</code> (для dropdown), <code>phase</code> (для неопределенного прогресса). Метод <code>tick(float dt)</code> автоматически вызывается из <code>window::update()</code> и интерполирует значения.</p>
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<section id="tips">
|
||||
<h2>💡 Советы и лучшие практики</h2>
|
||||
<ul>
|
||||
<li><strong>Производительность:</strong> Библиотека использует программный рендеринг (CPU). Для сложных сцен избегайте частого пересоздания <code>RawPicture</code>. Используйте <code>blit</code> для кеширования отрисовки.</li>
|
||||
<li><strong>ID Виджетов:</strong> Всегда задавайте уникальное поле <code>id</code> в <code>WidgetData</code>, если планируете обращаться к виджету позже или слушать его события.</li>
|
||||
<li><strong>Вложенность:</strong> Виджеты <code>TreeView</code> и <code>ScrollView</code> предназначены для содержания других виджетов. Добавляйте их через стандартные методы, они будут учитываться при расчете высоты контента и прокрутки.</li>
|
||||
<li><strong>Цвета:</strong> Указываются в формате HEX (например, <code>0xFF0000</code> для красного). Библиотека автоматически преобразует их в формат пикселей X11 при отрисовке.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<footer style="margin-top: 4rem; padding-top: 2rem; border-top: 1px solid var(--border); color: var(--text-secondary); text-align: center; font-size: 0.9rem;">
|
||||
<p>Сгенерировано для X11 C++ GUI Library. 2026</p>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<!-- Скрипты -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-cpp.min.js"></script>
|
||||
<script>
|
||||
// Функция копирования кода
|
||||
function copyCode(button) {
|
||||
const codeBlock = button.parentElement.nextElementSibling.querySelector('code');
|
||||
const text = codeBlock.innerText;
|
||||
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
const originalText = button.innerText;
|
||||
button.innerText = 'Скопировано!';
|
||||
button.classList.add('copied');
|
||||
|
||||
setTimeout(() => {
|
||||
button.innerText = originalText;
|
||||
button.classList.remove('copied');
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
// Поиск по документации
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
const sections = document.querySelectorAll('section');
|
||||
const navLinks = document.querySelectorAll('.nav-link');
|
||||
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
const term = e.target.value.toLowerCase();
|
||||
|
||||
sections.forEach(section => {
|
||||
const text = section.innerText.toLowerCase();
|
||||
if (text.includes(term)) {
|
||||
section.style.display = 'block';
|
||||
} else {
|
||||
section.style.display = term === '' ? 'block' : 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Подсветка активных ссылок в сайдбаре
|
||||
navLinks.forEach(link => {
|
||||
const targetId = link.getAttribute('href').substring(1);
|
||||
const targetSection = document.getElementById(targetId);
|
||||
if (targetSection && targetSection.style.display === 'none') {
|
||||
link.style.display = 'none';
|
||||
} else {
|
||||
link.style.display = 'block';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Плавная прокрутка и активный класс
|
||||
navLinks.forEach(link => {
|
||||
link.addEventListener('click', (e) => {
|
||||
navLinks.forEach(l => l.classList.remove('active'));
|
||||
link.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
let current = '';
|
||||
sections.forEach(section => {
|
||||
const sectionTop = section.offsetTop;
|
||||
if (pageYOffset >= sectionTop - 100) {
|
||||
current = section.getAttribute('id');
|
||||
}
|
||||
});
|
||||
|
||||
navLinks.forEach(link => {
|
||||
link.classList.remove('active');
|
||||
if (link.getAttribute('href').substring(1) === current) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
font/SGr-Iosevka-ExtraLight.ttc
Normal file
BIN
font/SGr-Iosevka-ExtraLight.ttc
Normal file
Binary file not shown.
BIN
font/SGr-Iosevka-Regular.ttc
Normal file
BIN
font/SGr-Iosevka-Regular.ttc
Normal file
Binary file not shown.
BIN
font/Terminus.ttf
Executable file
BIN
font/Terminus.ttf
Executable file
Binary file not shown.
120
gui/core/common.h
Normal file
120
gui/core/common.h
Normal file
@ -0,0 +1,120 @@
|
||||
#pragma once
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xutil.h>
|
||||
#include <X11/Xatom.h>
|
||||
#include <X11/keysym.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <iostream>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace gui {
|
||||
|
||||
constexpr int ROW = 40;
|
||||
|
||||
struct EventData {
|
||||
std::string source_id, text;
|
||||
float value = 0;
|
||||
int index = -1;
|
||||
bool flag = false;
|
||||
};
|
||||
|
||||
struct DebugInfo {
|
||||
int fps = 0, frame_count = 0, widget_count = 0, partial_updates = 0;
|
||||
float fps_time = 0, last_event_time = 0;
|
||||
int mouse_x = 0, mouse_y = 0;
|
||||
std::string last_event;
|
||||
std::deque<std::string> log;
|
||||
size_t max_log_size = 15;
|
||||
bool show_debug_overlay = false, show_dirty_rects = false;
|
||||
bool show_click_markers = true, show_widget_bounds = false, log_to_console = true;
|
||||
struct ClickMarker { int x, y; float ttl; uint32_t color; };
|
||||
struct DirtyFlash { int x, y, w, h; float ttl; };
|
||||
std::vector<ClickMarker> click_markers;
|
||||
std::vector<DirtyFlash> dirty_flashes;
|
||||
void add_log(const std::string& m) { log.push_back(m); if (log.size() > max_log_size) log.pop_front(); }
|
||||
};
|
||||
|
||||
class DirtyRegion {
|
||||
public:
|
||||
void add(int x, int y, int w, int h) {
|
||||
if (w <= 0 || h <= 0) return;
|
||||
if (x < 0) { w += x; x = 0; }
|
||||
if (y < 0) { h += y; y = 0; }
|
||||
rects.push_back({(short)x, (short)y, (unsigned short)w, (unsigned short)h});
|
||||
dirty = true;
|
||||
}
|
||||
void add(const XRectangle& r) { rects.push_back(r); dirty = true; }
|
||||
void mark_all(int w, int h) { add(0, 0, w, h); }
|
||||
bool is_dirty() const { return dirty; }
|
||||
bool intersects(int x, int y, int w, int h) const {
|
||||
for (auto& r : rects) {
|
||||
int x1 = std::max((int)r.x, x), y1 = std::max((int)r.y, y);
|
||||
int x2 = std::min((int)r.x + r.width, x + w), y2 = std::min((int)r.y + r.height, y + h);
|
||||
if (x2 > x1 && y2 > y1) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const std::vector<XRectangle>& get() const { return rects; }
|
||||
void clear() { rects.clear(); dirty = false; }
|
||||
private:
|
||||
std::vector<XRectangle> rects;
|
||||
bool dirty = false;
|
||||
};
|
||||
|
||||
inline float ease(float t) { return t * t * (3 - 2 * t); }
|
||||
inline float approach(float cur, float target, float dt, float speed = 14) {
|
||||
cur += (target - cur) * std::min(1.f, dt * speed);
|
||||
if (std::abs(target - cur) < 0.001f) cur = target;
|
||||
return cur;
|
||||
}
|
||||
inline uint32_t mixc(uint32_t a, uint32_t b, float t) {
|
||||
if (t <= 0.f) return a;
|
||||
if (t >= 1.f) return b;
|
||||
int r = (int)(((a >> 16) & 255) + (((b >> 16) & 255) - ((a >> 16) & 255)) * t);
|
||||
int g = (int)(((a >> 8) & 255) + (((b >> 8) & 255) - ((a >> 8) & 255)) * t);
|
||||
int bl = (int)((a & 255) + ((b & 255) - (a & 255)) * t);
|
||||
return ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)bl;
|
||||
}
|
||||
|
||||
inline void x_fill_round(Display* d, Drawable w, GC gc, int x, int y, int W, int H, int r) {
|
||||
if (r <= 0) { XFillRectangle(d, w, gc, x, y, W, H); return; }
|
||||
r = std::min(r, std::min(W / 2, H / 2));
|
||||
|
||||
// центральные прямоугольники (на fill_style не чувствительны, но поставим для надёжности)
|
||||
XSetFillStyle(d, gc, FillSolid);
|
||||
XFillRectangle(d, w, gc, x + r, y, W - 2 * r, H);
|
||||
XFillRectangle(d, w, gc, x, y + r, W, H - 2 * r);
|
||||
|
||||
// арки: жёстко фиксируем режим перед каждым вызовом
|
||||
XSetArcMode(d, gc, ArcPieSlice);
|
||||
XSetFillStyle(d, gc, FillSolid);
|
||||
XFillArc(d, w, gc, x, y, 2 * r, 2 * r, 90 * 64, 90 * 64);
|
||||
XFillArc(d, w, gc, x + W - 2 * r, y, 2 * r, 2 * r, 0 * 64, 90 * 64);
|
||||
XFillArc(d, w, gc, x, y + H - 2 * r, 2 * r, 2 * r, 180 * 64, 90 * 64);
|
||||
XFillArc(d, w, gc, x + W - 2 * r, y + H - 2 * r, 2 * r, 2 * r, 270 * 64, 90 * 64);
|
||||
}
|
||||
|
||||
inline void x_draw_round(Display* d, Drawable w, GC gc, int x, int y, int W, int H, int r) {
|
||||
if (r <= 0) { XDrawRectangle(d, w, gc, x, y, W - 1, H - 1); return; }
|
||||
r = std::min(r, std::min(W / 2, H / 2));
|
||||
|
||||
XDrawLine(d, w, gc, x + r, y, x + W - r - 1, y);
|
||||
XDrawLine(d, w, gc, x + r, y + H - 1, x + W - r - 1, y + H - 1);
|
||||
XDrawLine(d, w, gc, x, y + r, x, y + H - r - 1);
|
||||
XDrawLine(d, w, gc, x + W - 1, y + r, x + W - 1, y + H - r - 1);
|
||||
|
||||
XSetArcMode(d, gc, ArcPieSlice);
|
||||
XSetFillStyle(d, gc, FillSolid);
|
||||
XDrawArc(d, w, gc, x, y, 2 * r, 2 * r, 90 * 64, 90 * 64);
|
||||
XDrawArc(d, w, gc, x + W - 2*r - 1, y, 2 * r, 2 * r, 0 * 64, 90 * 64);
|
||||
XDrawArc(d, w, gc, x, y + H - 2*r - 1, 2 * r, 2 * r, 180 * 64, 90 * 64);
|
||||
XDrawArc(d, w, gc, x + W - 2*r - 1, y + H - 2*r - 1, 2 * r, 2 * r, 270 * 64, 90 * 64);
|
||||
}
|
||||
}
|
||||
20
gui/core/event_bus.cpp
Normal file
20
gui/core/event_bus.cpp
Normal file
@ -0,0 +1,20 @@
|
||||
#include "event_bus.h"
|
||||
namespace gui {
|
||||
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& [n, 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) {
|
||||
if (global_logger) global_logger(e, d);
|
||||
auto it = handlers.find(e);
|
||||
if (it == handlers.end()) return;
|
||||
auto copy = it->second;
|
||||
for (auto& [t, h] : copy) h(d);
|
||||
}
|
||||
}
|
||||
17
gui/core/event_bus.h
Normal file
17
gui/core/event_bus.h
Normal file
@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
#include "common.h"
|
||||
namespace gui {
|
||||
class EventBus {
|
||||
public:
|
||||
using Handler = std::function<void(const EventData&)>;
|
||||
static EventBus& get();
|
||||
int on(const std::string& event, Handler h);
|
||||
void off(int token);
|
||||
void emit(const std::string& event, EventData data = {});
|
||||
void set_global_logger(std::function<void(const std::string&, const EventData&)> l) { global_logger = l; }
|
||||
private:
|
||||
int next_token = 1;
|
||||
std::unordered_map<std::string, std::vector<std::pair<int, Handler>>> handlers;
|
||||
std::function<void(const std::string&, const EventData&)> global_logger;
|
||||
};
|
||||
}
|
||||
160
gui/core/font_manager.cpp
Normal file
160
gui/core/font_manager.cpp
Normal file
@ -0,0 +1,160 @@
|
||||
#include "font_manager.h"
|
||||
#include <ft2build.h>
|
||||
#include FT_FREETYPE_H
|
||||
namespace gui {
|
||||
static int g_ascent = 11, g_descent = 2, g_charw = 6;
|
||||
static Font g_core_font = 0;
|
||||
void font_init_fallback(Display* dpy, GC gc) {
|
||||
const char* names[] = {"10x20", "9x15", "8x13", "fixed", nullptr};
|
||||
for (int i = 0; names[i] && !g_core_font; i++) g_core_font = XLoadFont(dpy, names[i]);
|
||||
if (!g_core_font) return;
|
||||
XSetFont(dpy, gc, g_core_font);
|
||||
XFontStruct* fs = XQueryFont(dpy, g_core_font);
|
||||
if (fs) { g_ascent = fs->ascent; g_descent = fs->descent;
|
||||
g_charw = fs->max_bounds.width ? fs->max_bounds.width : 6; XFreeFont(dpy, fs); }
|
||||
}
|
||||
|
||||
FontManager& FontManager::get() { static FontManager inst; return inst; }
|
||||
FontManager::~FontManager() {
|
||||
if (face) FT_Done_Face((FT_Face)face);
|
||||
if (lib) FT_Done_FreeType((FT_Library)lib);
|
||||
}
|
||||
bool FontManager::load(const std::string& path) {
|
||||
if (!lib && FT_Init_FreeType((FT_Library*)&lib)) { lib = nullptr; return false; }
|
||||
FT_Face f = nullptr;
|
||||
if (FT_New_Face((FT_Library)lib, path.c_str(), 0, &f)) return false;
|
||||
if (face) FT_Done_Face((FT_Face)face);
|
||||
face = f; ok = true; cur_size = -1;
|
||||
set_size(def_size);
|
||||
return true;
|
||||
}
|
||||
void FontManager::set_size(int s) const {
|
||||
if (!ok || s == cur_size) return;
|
||||
FT_Set_Pixel_Sizes((FT_Face)face, 0, s);
|
||||
cur_size = s;
|
||||
}
|
||||
int FontManager::ascent(int size) const {
|
||||
if (!ok) return g_ascent;
|
||||
set_size(resolve(size));
|
||||
return (int)(((FT_Face)face)->size->metrics.ascender >> 6);
|
||||
}
|
||||
int FontManager::descent(int size) const {
|
||||
if (!ok) return g_descent;
|
||||
set_size(resolve(size));
|
||||
return (int)(-((FT_Face)face)->size->metrics.descender >> 6);
|
||||
}
|
||||
int FontManager::line_height(int size) const {
|
||||
if (!ok) return g_ascent + g_descent;
|
||||
set_size(resolve(size));
|
||||
return (int)(((FT_Face)face)->size->metrics.height >> 6);
|
||||
}
|
||||
int FontManager::text_width(const std::string& t, int size) const {
|
||||
if (!ok) return (int)t.size() * g_charw;
|
||||
set_size(resolve(size));
|
||||
FT_Face f = (FT_Face)face;
|
||||
int w = 0;
|
||||
for (char c : t) { if (!FT_Load_Char(f, c, FT_LOAD_DEFAULT)) w += (int)(f->glyph->advance.x >> 6); }
|
||||
return w;
|
||||
}
|
||||
|
||||
// ==== TEXT CACHE: 1-bit mask + FillStippled (no XGetImage, very fast) ====
|
||||
struct MaskEntry { Pixmap mask = 0; int w = 0, h = 0, asc = 0; };
|
||||
static std::unordered_map<std::string, MaskEntry> tcache;
|
||||
|
||||
void FontManager::render_masked(Display* dpy, Drawable win, GC gc, int x, int y,
|
||||
const std::string& t, uint32_t color, int size) const {
|
||||
size = resolve(size);
|
||||
std::string key = t + '\n' + std::to_string(size);
|
||||
auto it = tcache.find(key);
|
||||
MaskEntry e;
|
||||
if (it == tcache.end()) {
|
||||
set_size(size);
|
||||
FT_Face f = (FT_Face)face;
|
||||
int asc = (int)(f->size->metrics.ascender >> 6);
|
||||
int lh = (int)(f->size->metrics.height >> 6);
|
||||
int w = 0;
|
||||
for (char c : t) { if (!FT_Load_Char(f, c, FT_LOAD_DEFAULT)) w += (int)(f->glyph->advance.x >> 6); }
|
||||
if (w <= 0 || lh <= 0) { tcache[key] = e; return; }
|
||||
std::vector<uint8_t> alpha((size_t)w * lh, 0);
|
||||
long pen = 0;
|
||||
for (char c : t) {
|
||||
if (FT_Load_Char(f, c, FT_LOAD_RENDER)) { pen += 6; continue; }
|
||||
FT_GlyphSlot g = f->glyph;
|
||||
FT_Bitmap& bmp = g->bitmap;
|
||||
int bx = (int)(pen + g->bitmap_left), by = asc - (int)g->bitmap_top;
|
||||
for (unsigned j = 0; j < bmp.rows; j++)
|
||||
for (unsigned i = 0; i < bmp.width; i++) {
|
||||
int px = bx + (int)i, py = by + (int)j;
|
||||
if (px < 0 || py < 0 || px >= w || py >= lh) continue;
|
||||
auto& slot = alpha[(size_t)py * w + px];
|
||||
slot = std::max(slot, bmp.buffer[j * bmp.pitch + i]);
|
||||
}
|
||||
pen += g->advance.x >> 6;
|
||||
}
|
||||
int scr = DefaultScreen(dpy);
|
||||
XImage* im = XCreateImage(dpy, DefaultVisual(dpy, scr), 1, ZPixmap, 0,
|
||||
(char*)calloc((size_t)((w + 31) / 32) * 4 * lh, 1),
|
||||
(unsigned)w, (unsigned)lh, 32, 0);
|
||||
if (!im) return;
|
||||
for (int py = 0; py < lh; py++)
|
||||
for (int px = 0; px < w; px++)
|
||||
if (alpha[(size_t)py * w + px] >= 100) XPutPixel(im, px, py, 1);
|
||||
Pixmap pm = XCreatePixmap(dpy, RootWindow(dpy, scr), w, lh, 1);
|
||||
GC g2 = XCreateGC(dpy, pm, 0, nullptr);
|
||||
XPutImage(dpy, pm, g2, im, 0, 0, 0, 0, (unsigned)w, (unsigned)lh);
|
||||
XFreeGC(dpy, g2);
|
||||
XDestroyImage(im);
|
||||
e = {pm, w, lh, asc};
|
||||
if (tcache.size() > 500) {
|
||||
for (auto& [k, v] : tcache) if (v.mask) XFreePixmap(dpy, v.mask);
|
||||
tcache.clear();
|
||||
}
|
||||
tcache[key] = e;
|
||||
} else e = it->second;
|
||||
if (!e.mask) return;
|
||||
XSetForeground(dpy, gc, color);
|
||||
XSetFillStyle(dpy, gc, FillStippled);
|
||||
XSetStipple(dpy, gc, e.mask);
|
||||
XSetTSOrigin(dpy, gc, x, y - e.asc);
|
||||
XFillRectangle(dpy, win, gc, x, y - e.asc, (unsigned)e.w, (unsigned)e.h);
|
||||
XSetFillStyle(dpy, gc, FillSolid);
|
||||
}
|
||||
|
||||
void FontManager::draw(RawPicture& dst, int x, int y, const std::string& t, uint32_t color, int size) const {
|
||||
if (!ok || t.empty()) return;
|
||||
set_size(resolve(size));
|
||||
FT_Face f = (FT_Face)face;
|
||||
long pen = 0;
|
||||
for (char c : t) {
|
||||
if (FT_Load_Char(f, c, FT_LOAD_RENDER)) { pen += 6; continue; }
|
||||
FT_GlyphSlot g = f->glyph;
|
||||
FT_Bitmap& bmp = g->bitmap;
|
||||
int bx = (int)(pen + g->bitmap_left), by = y - (int)g->bitmap_top;
|
||||
for (unsigned j = 0; j < bmp.rows; j++)
|
||||
for (unsigned i = 0; i < bmp.width; i++) {
|
||||
uint8_t a = bmp.buffer[j * bmp.pitch + i];
|
||||
if (a) dst.blendPixel(bx + (int)i, by + (int)j, color, a);
|
||||
}
|
||||
pen += g->advance.x >> 6;
|
||||
}
|
||||
}
|
||||
void FontManager::draw(Display* dpy, Drawable win, GC gc, int x, int y, const std::string& t, uint32_t color, int size) const {
|
||||
if (!ok || t.empty()) { XDrawString(dpy, win, gc, x, y, t.c_str(), (int)t.size()); return; }
|
||||
render_masked(dpy, win, gc, x, y, t, color, size);
|
||||
}
|
||||
|
||||
void gui_text(Display* d, Drawable w, GC gc, int x, int y, const std::string& t, uint32_t c, int size) {
|
||||
auto& fm = FontManager::get();
|
||||
if (fm.available()) fm.draw(d, w, gc, x, y, t, c, size);
|
||||
else { XSetForeground(d, gc, c); XDrawString(d, w, gc, x, y, t.c_str(), (int)t.size()); }
|
||||
}
|
||||
int gui_text_width(const std::string& t, int size) { return FontManager::get().text_width(t, size); }
|
||||
int gui_base_c(int top, int h, int size) {
|
||||
auto& fm = FontManager::get();
|
||||
return top + h / 2 + (fm.ascent(size) - fm.descent(size)) / 2;
|
||||
}
|
||||
int gui_base_row(int top, int size) {
|
||||
auto& fm = FontManager::get();
|
||||
return top + (ROW + fm.ascent(size) - fm.descent(size)) / 2;
|
||||
}
|
||||
}
|
||||
35
gui/core/font_manager.h
Normal file
35
gui/core/font_manager.h
Normal file
@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
#include "common.h"
|
||||
#include "raw_picture.h"
|
||||
namespace gui {
|
||||
class FontManager {
|
||||
public:
|
||||
static FontManager& get();
|
||||
~FontManager();
|
||||
bool load(const std::string& path);
|
||||
bool available() const { return ok; }
|
||||
void set_default_size(int px) { def_size = px; }
|
||||
int default_size() const { return def_size; }
|
||||
int ascent(int size = 0) const;
|
||||
int descent(int size = 0) const;
|
||||
int line_height(int size = 0) const;
|
||||
int text_width(const std::string& t, int size = 0) const;
|
||||
void draw(RawPicture& dst, int x, int y, const std::string& t, uint32_t color, int size = 0) const;
|
||||
void draw(Display*, Drawable win, GC gc, int x, int y, const std::string& t, uint32_t color, int size = 0) const;
|
||||
private:
|
||||
FontManager() = default;
|
||||
void* lib = nullptr;
|
||||
void* face = nullptr;
|
||||
bool ok = false;
|
||||
int def_size = 32;
|
||||
mutable int cur_size = -1;
|
||||
void set_size(int s) const;
|
||||
int resolve(int s) const { return s > 0 ? s : def_size; }
|
||||
void render_masked(Display*, Drawable, GC, int x, int y, const std::string&, uint32_t color, int size) const;
|
||||
};
|
||||
void font_init_fallback(Display*, GC);
|
||||
void gui_text(Display*, Drawable, GC, int x, int y, const std::string&, uint32_t, int size);
|
||||
int gui_text_width(const std::string&, int size);
|
||||
int gui_base_c(int top, int h, int size);
|
||||
int gui_base_row(int top, int size);
|
||||
}
|
||||
179
gui/core/raw_picture.cpp
Normal file
179
gui/core/raw_picture.cpp
Normal file
@ -0,0 +1,179 @@
|
||||
#include "raw_picture.h"
|
||||
#include "font_manager.h"
|
||||
#include <fstream>
|
||||
namespace gui {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
void RawPicture::blendPixel(int x, int y, uint32_t c, uint8_t a) {
|
||||
if (a == 255) { setPixel(x, y, c); return; }
|
||||
if (x < 0 || y < 0 || x >= w || y >= h) return;
|
||||
uint32_t bg = getPixel(x, y);
|
||||
uint8_t cr = (c >> 16) & 255, cg = (c >> 8) & 255, cb = c & 255;
|
||||
uint8_t br = (bg >> 16) & 255, bgg = (bg >> 8) & 255, bb = bg & 255;
|
||||
setPixel(x, y, ((uint32_t)((cr * a + br * (255 - a)) / 255) << 16) |
|
||||
((uint32_t)((cg * a + bgg * (255 - a)) / 255) << 8) |
|
||||
(uint32_t)((cb * a + bb * (255 - a)) / 255));
|
||||
}
|
||||
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::fillRoundRect(int x, int y, int W, int H, int r, uint32_t c) {
|
||||
if (r <= 0) { fillRect(x, y, W, H, c); return; }
|
||||
r = std::min(r, std::min(W / 2, H / 2));
|
||||
fillRect(x + r, y, W - 2 * r, H, c);
|
||||
fillRect(x, y + r, W, H - 2 * r, c);
|
||||
fillCircle(x + r, y + r, r, c);
|
||||
fillCircle(x + W - r - 1, y + r, r, c);
|
||||
fillCircle(x + r, y + H - r - 1, r, c);
|
||||
fillCircle(x + W - r - 1, y + H - r - 1, r, 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))
|
||||
xs.push_back(pts[i].first + (int)((double)(y - y0) / (y1 - y0) * (pts[j].first - pts[i].first)));
|
||||
}
|
||||
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::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) {
|
||||
auto& fm = FontManager::get();
|
||||
if (fm.available()) {
|
||||
int size = fm.default_size() * std::max(1, scale);
|
||||
fm.draw(*this, x, y + fm.ascent(size), t, c, size);
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < t.size(); i++)
|
||||
fillRect(x + (int)i * 6 * scale, y, 5 * scale, 8 * scale, c);
|
||||
}
|
||||
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, Drawable 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);
|
||||
}
|
||||
}
|
||||
31
gui/core/raw_picture.h
Normal file
31
gui/core/raw_picture.h
Normal file
@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
#include "common.h"
|
||||
namespace gui {
|
||||
class RawPicture {
|
||||
public:
|
||||
RawPicture(int w = 0, int h = 0, int bits = 32) { create(w, h, bits); }
|
||||
void create(int w, int h, int bits);
|
||||
int width() const { return w; }
|
||||
int height() const { return h; }
|
||||
void clear(uint32_t c = 0);
|
||||
void setPixel(int x, int y, uint32_t c);
|
||||
void blendPixel(int x, int y, uint32_t c, uint8_t a);
|
||||
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 fillRoundRect(int x, int y, int w, int h, int r, 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 fillTriangle(int x1, int y1, int x2, int y2, int x3, int y3, uint32_t c);
|
||||
void fillPolygon(const std::vector<std::pair<int,int>>& pts, uint32_t c);
|
||||
void drawText(Display*, 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*, Drawable win, GC gc, int dx, int dy) const;
|
||||
private:
|
||||
int w = 0, h = 0, bits = 32;
|
||||
std::vector<uint8_t> buf;
|
||||
int bpp() const { return bits / 8; }
|
||||
};
|
||||
}
|
||||
30
gui/core/theme.cpp
Normal file
30
gui/core/theme.cpp
Normal file
@ -0,0 +1,30 @@
|
||||
#include "theme.h"
|
||||
#include "font_manager.h"
|
||||
namespace gui {
|
||||
static Theme t_term{
|
||||
ThemeStyle::TERMINAL, 0, 0, 32,
|
||||
0x121212, 0x1A1A1A, 0xFFFFFF, 0x888888,
|
||||
0x1A1A1A, 0x232323, 0x0E0E0E, 0x00CC00, 0x66FF66,
|
||||
0x00AA00, 0x00CC00, 0x009900, 0x000000,
|
||||
0x101010, 0x00FF00,
|
||||
0x333333, 0x111111,
|
||||
0x141414, 0x1E1E1E, 0x00CC00, 0x000000,
|
||||
0x161616, 0x1E1E1E, 0x242424
|
||||
};
|
||||
static Theme t_win11{
|
||||
ThemeStyle::WIN11, 12, 8, 32,
|
||||
0x202020, 0x2B2B2B, 0xFFFFFF, 0x9D9D9D,
|
||||
0x2D2D2D, 0x3A3A3A, 0x262626, 0x3B3B3B, 0x5A5A5A,
|
||||
0x0078D4, 0x2488E0, 0x0063B1, 0xFFFFFF,
|
||||
0x1A1A1A, 0x4CC2FF,
|
||||
0x3A3A3A, 0x2A2A2A,
|
||||
0x262626, 0x333333, 0x0078D4, 0xFFFFFF,
|
||||
0x242424, 0x2E2E2E, 0x333333
|
||||
};
|
||||
static ThemeStyle cur = ThemeStyle::WIN11;
|
||||
const Theme& theme() { return cur == ThemeStyle::WIN11 ? t_win11 : t_term; }
|
||||
void set_theme(ThemeStyle s) {
|
||||
cur = s;
|
||||
FontManager::get().set_default_size(theme().font_size);
|
||||
}
|
||||
}
|
||||
18
gui/core/theme.h
Normal file
18
gui/core/theme.h
Normal file
@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
#include "common.h"
|
||||
namespace gui {
|
||||
enum class ThemeStyle { TERMINAL, WIN11 };
|
||||
struct Theme {
|
||||
ThemeStyle style = ThemeStyle::TERMINAL;
|
||||
int radius = 0, radius_sm = 0, font_size = 32;
|
||||
uint32_t window_bg, panel_bg, text, text_dim;
|
||||
uint32_t btn, btn_h, btn_p, border, border_h;
|
||||
uint32_t accent, accent_h, accent_p, on_accent;
|
||||
uint32_t input_bg, focus;
|
||||
uint32_t track, prog_bg;
|
||||
uint32_t list_bg, list_h, list_sel, list_sel_text;
|
||||
uint32_t tab_bg, tab_h, tab_active;
|
||||
};
|
||||
const Theme& theme();
|
||||
void set_theme(ThemeStyle s);
|
||||
}
|
||||
115
gui/demo.cpp
Normal file
115
gui/demo.cpp
Normal file
@ -0,0 +1,115 @@
|
||||
#include "gui.h"
|
||||
namespace gui {
|
||||
void run_demo() {
|
||||
set_theme(ThemeStyle::WIN11);
|
||||
window win("GUI Demo");
|
||||
win.set_font("SGr-Iosevka-Regular.ttc");
|
||||
win.set_default_icon();
|
||||
|
||||
win.on_key = [&](KeySym ks) {
|
||||
if (ks == XK_F6) {
|
||||
set_theme(theme().style == ThemeStyle::WIN11 ? ThemeStyle::TERMINAL : ThemeStyle::WIN11);
|
||||
win.mark_all_dirty();
|
||||
}
|
||||
};
|
||||
|
||||
for (auto& name : {"controls", "lists", "graphics"}) {
|
||||
WidgetData tab;
|
||||
tab.id = name; tab.text = name;
|
||||
tab.on_window_click = [&win, name](window& w) { w.openPAGE(name); };
|
||||
win.addTAB_BUTTON(tab);
|
||||
}
|
||||
|
||||
// ---------------- controls ----------------
|
||||
win.configur_page("controls", [](page& p) {
|
||||
WidgetData d;
|
||||
d.text = "GUI Demo"; d.x = 400; d.y = 200; d.width = 600; d.height = 80;
|
||||
d.font_size = 48; p.Label(d);
|
||||
|
||||
d = {}; d.id = "btn1"; d.text = "Regular Button"; d.x = 400; d.y = 320; d.width = 360; d.height = 72;
|
||||
p.Button(d);
|
||||
d = {}; d.id = "btn2"; d.text = "Accent"; d.primary = true; d.x = 800; d.y = 320; d.width = 300; d.height = 72;
|
||||
p.Button(d);
|
||||
|
||||
d = {}; d.id = "inp"; d.text = ""; d.x = 400; d.y = 440; d.width = 500; d.height = 68;
|
||||
p.Input(d);
|
||||
|
||||
d = {}; d.id = "chk"; d.text = "Animated checkbox"; d.x = 400; d.y = 540; d.width = 520; d.height = 56;
|
||||
p.Checkbox(d);
|
||||
d = {}; d.id = "r1"; d.text = "Option A"; d.x = 400; d.y = 620; d.width = 300; d.height = 52; d.checked = true;
|
||||
p.Radiobutton(d);
|
||||
d = {}; d.id = "r1"; d.text = "Option B"; d.x = 740; d.y = 620; d.width = 300; d.height = 52;
|
||||
p.Radiobutton(d);
|
||||
|
||||
d = {}; d.id = "sld"; d.x = 400; d.y = 720; d.width = 600; d.height = 48; d.value = 0.4f;
|
||||
p.Slider(d);
|
||||
d = {}; d.id = "prog"; d.x = 400; d.y = 800; d.width = 600; d.height = 44; d.target_value = 0.4f;
|
||||
p.Progressbar(d);
|
||||
d = {}; d.id = "anim"; d.text = "Indeterminate"; d.x = 1040; d.y = 800; d.width = 340; d.height = 52;
|
||||
p.Checkbox(d);
|
||||
}, true);
|
||||
|
||||
// ---------------- lists ----------------
|
||||
win.configur_page("lists", [](page& p) {
|
||||
WidgetData d;
|
||||
d.id = "lb"; d.x = 400; d.y = 200; d.width = 440; d.height = 320;
|
||||
d.items = {"Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6", "Item 7", "Item 8"};
|
||||
p.Listbox(d);
|
||||
|
||||
d = {}; d.id = "dd"; d.text = "Select..."; d.x = 900; d.y = 200; d.width = 400; d.height = 68;
|
||||
d.items = {"Option A", "Option B", "Option C"};
|
||||
p.Dropdown(d);
|
||||
|
||||
d = {}; d.id = "tree"; d.x = 400; d.y = 580; d.width = 600; d.height = 280;
|
||||
auto& tv = p.TreeView(d);
|
||||
|
||||
WidgetData n; n.id = "n1"; n.text = "Node 1"; n.expanded = true;
|
||||
auto& n1 = tv.TreeNode(n);
|
||||
|
||||
WidgetData l; l.text = "child element"; l.width = 360; l.height = 44;
|
||||
n1.Label(l);
|
||||
|
||||
n = {}; n.id = "n2"; n.text = "Node 2";
|
||||
auto& n2 = n1.TreeNode(n);
|
||||
|
||||
|
||||
l.text = "child element"; l.width = 360; l.height = 44;
|
||||
n2.Label(l);
|
||||
|
||||
d = {}; d.id = "scr"; d.x = 1060; d.y = 580; d.width = 440; d.height = 280;
|
||||
auto& sv = p.ScrollView(d);
|
||||
for (int i = 0; i < 8; i++) {
|
||||
WidgetData b; b.id = "sb" + std::to_string(i); b.text = "Button " + std::to_string(i);
|
||||
b.x = 10; b.y = 8 + i * 80; b.width = 400; b.height = 68;
|
||||
sv.Button(b);
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------- graphics ----------------
|
||||
win.configur_page("graphics", [](page& p) {
|
||||
auto pic = std::make_shared<RawPicture>(600, 400, 32);
|
||||
pic->clear(0x1A1A2E);
|
||||
pic->fillRoundRect(20, 20, 560, 360, 32, 0x16213E);
|
||||
pic->fillCircle(160, 160, 80, 0x0F3460);
|
||||
pic->fillCircle(160, 160, 50, 0x533483);
|
||||
pic->fillTriangle(300, 320, 440, 120, 560, 320, 0xE94560);
|
||||
WidgetData d; d.id = "pic"; d.x = 400; d.y = 200; d.width = 600; d.height = 400;
|
||||
d.picture = pic;
|
||||
p.RawPictureWidget(d);
|
||||
});
|
||||
|
||||
// ---------------- wiring ----------------
|
||||
EventBus::get().on("slider.change", [&win](const EventData& e) {
|
||||
if (auto* w = win.find("prog")) { w->setValue(e.value); win.mark_dirty(400, 800, 600, 44); }
|
||||
});
|
||||
EventBus::get().on("checkbox.change", [&win](const EventData& e) {
|
||||
if (e.source_id == "anim")
|
||||
if (auto* w = win.find("prog")) { w->data.indeterminate = e.flag; win.mark_dirty(400, 800, 600, 44); }
|
||||
});
|
||||
EventBus::get().on("button.click", [](const EventData& e) {
|
||||
std::cout << ">> click: " << e.source_id << "\n";
|
||||
});
|
||||
|
||||
while (win.running()) win.update();
|
||||
}
|
||||
}
|
||||
6
gui/demo.h
Normal file
6
gui/demo.h
Normal file
@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
namespace gui {
|
||||
// Full-featured demo: all widgets, both themes, animations, debug overlay.
|
||||
// Hotkeys: F1..F5 debug, F6 switch theme.
|
||||
void run_demo();
|
||||
}
|
||||
9
gui/gui.h
Normal file
9
gui/gui.h
Normal file
@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
#include "core/common.h"
|
||||
#include "core/event_bus.h"
|
||||
#include "core/raw_picture.h"
|
||||
#include "core/font_manager.h"
|
||||
#include "core/theme.h"
|
||||
#include "widgets/widget.h"
|
||||
#include "window.h"
|
||||
#include "demo.h"
|
||||
49
gui/widgets/draw_basic.cpp
Normal file
49
gui/widgets/draw_basic.cpp
Normal file
@ -0,0 +1,49 @@
|
||||
#include "widget.h"
|
||||
namespace gui {
|
||||
|
||||
void wdraw::basic(Display* d, Drawable win, GC gc, const Widget& w, int ax, int ay, OverlayList* ov) {
|
||||
(void)ov;
|
||||
const auto& t = theme();
|
||||
const auto& dt = w.data;
|
||||
int fs = dt.font_size ? dt.font_size : t.font_size;
|
||||
|
||||
if (dt.type == WidgetType::LABEL) {
|
||||
gui_text(d, win, gc, ax, gui_base_c(ay, dt.height, fs), dt.text,
|
||||
C(dt.fg_color, t.text), fs);
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t bg, bd, txt;
|
||||
if (dt.primary) {
|
||||
float p = w.pressed ? 0.5f : 0.f;
|
||||
bg = mixc(mixc(C(dt.bg_color, t.btn), C(dt.bg_color, t.btn_h), w.hover_a), t.btn_p, p);
|
||||
bd = bg; txt = t.on_accent;
|
||||
} else {
|
||||
float p = w.pressed ? 0.5f : 0.f;
|
||||
bg = mixc(mixc(t.accent, t.accent_h, w.hover_a),t.accent_p, p * 0.5f);
|
||||
bd = mixc(C(dt.frame_color, t.border), t.border_h, w.hover_a);
|
||||
txt = C(dt.fg_color, t.text);
|
||||
}
|
||||
|
||||
if (dt.type == WidgetType::SPRITE_BTN && dt.picture) {
|
||||
dt.picture->present(d, win, gc, ax, ay);
|
||||
} else {
|
||||
XSetForeground(d, gc, bg);
|
||||
x_fill_round(d, win, gc, ax, ay, dt.width, dt.height, t.radius);
|
||||
}
|
||||
XSetForeground(d, gc, bd);
|
||||
x_draw_round(d, win, gc, ax, ay, dt.width, dt.height, t.radius);
|
||||
|
||||
if (dt.type == WidgetType::INPUT) {
|
||||
// фон поля ввода + нижняя акцент при фокусе
|
||||
XSetForeground(d, gc, C(dt.bg_color, t.input_bg));
|
||||
x_fill_round(d, win, gc, ax + 1, ay + 1, dt.width - 2, dt.height - 2, std::max(0, t.radius - 1));
|
||||
XSetForeground(d, gc, mixc(t.border, t.focus, w.hover_a));
|
||||
XFillRectangle(d, win, gc, ax + t.radius, ay + dt.height - 2, dt.width - 2 * t.radius, 2);
|
||||
int cx = ax + 6 + gui_text_width(dt.text, fs);
|
||||
XSetForeground(d, gc, t.focus);
|
||||
XDrawLine(d, win, gc, cx, ay + 6, cx, ay + dt.height - 6);
|
||||
}
|
||||
gui_text(d, win, gc, ax + 6, gui_base_c(ay, dt.height, fs), dt.text, txt, fs);
|
||||
}
|
||||
}
|
||||
118
gui/widgets/draw_lists.cpp
Normal file
118
gui/widgets/draw_lists.cpp
Normal file
@ -0,0 +1,118 @@
|
||||
#include "widget.h"
|
||||
namespace gui {
|
||||
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);
|
||||
}
|
||||
|
||||
void wdraw::lists(Display* d, Drawable win, GC gc, const Widget& w, int ax, int ay,
|
||||
const XRectangle& clip, OverlayList* ov) {
|
||||
const auto& t = theme();
|
||||
const auto& dt = w.data;
|
||||
int fs = dt.font_size ? dt.font_size : t.font_size;
|
||||
|
||||
if (dt.type == WidgetType::RAW_PIC) {
|
||||
if (dt.picture) dt.picture->present(d, win, gc, ax, ay);
|
||||
return;
|
||||
}
|
||||
|
||||
if (dt.type == WidgetType::DROPDOWN) {
|
||||
uint32_t bg = mixc(mixc(t.btn, t.btn_h, w.hover_a), t.btn_p, w.press_a);
|
||||
XSetForeground(d, gc, bg);
|
||||
x_fill_round(d, win, gc, ax, ay, dt.width, dt.height, t.radius);
|
||||
XSetForeground(d, gc, mixc(t.border, t.focus, w.hover_a));
|
||||
x_draw_round(d, win, gc, ax, ay, dt.width, dt.height, t.radius);
|
||||
std::string disp = (dt.selected_index >= 0 && dt.selected_index < (int)dt.items.size())
|
||||
? dt.items[dt.selected_index] : dt.text;
|
||||
gui_text(d, win, gc, ax + 12, gui_base_c(ay, dt.height, fs), disp, t.text, fs);
|
||||
gui_text(d, win, gc, ax + dt.width - 28, gui_base_c(ay, dt.height, fs),
|
||||
w.open_target ? "^" : "v", t.text_dim, fs);
|
||||
if (ov && w.anim > 0.01f) ov->push_back(&w);
|
||||
return;
|
||||
}
|
||||
|
||||
XSetForeground(d, gc, t.list_bg);
|
||||
x_fill_round(d, win, gc, ax, ay, dt.width, dt.height, t.radius);
|
||||
XSetForeground(d, gc, t.border);
|
||||
x_draw_round(d, win, gc, ax, ay, dt.width, dt.height, t.radius);
|
||||
|
||||
XRectangle vp = intersect(clip, {(short)(ax + 2), (short)(ay + 2),
|
||||
(unsigned short)(dt.width - 4), (unsigned short)(dt.height - 4)});
|
||||
set_clip(d, gc, vp);
|
||||
|
||||
if (dt.type == WidgetType::TREE_VIEW) {
|
||||
std::function<int(const Widget&, int, int)> drawN = [&](const Widget& n, int depth, int y) -> int {
|
||||
int nx = ax + 8 + depth * 28;
|
||||
gui_text(d, win, gc, nx, gui_base_row(y, fs), n.data.expanded ? "[-]" : "[+]", t.accent, fs);
|
||||
gui_text(d, win, gc, nx + 60, gui_base_row(y, fs), n.data.text, t.text, fs);
|
||||
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 + 60, y - dt.scroll, vp, ov); y += c->data.height + 4; }
|
||||
}
|
||||
return y;
|
||||
};
|
||||
int y = ay + 4 - dt.scroll;
|
||||
for (auto& c : w.widgets) y = drawN(*c, 0, y);
|
||||
} else {
|
||||
int cy = ay + 2 - dt.scroll;
|
||||
for (size_t i = 0; i < dt.items.size(); i++) {
|
||||
if ((int)i == dt.selected_index) {
|
||||
XSetForeground(d, gc, t.list_sel);
|
||||
x_fill_round(d, win, gc, ax + 4, cy, dt.width - 8, ROW, t.radius_sm);
|
||||
gui_text(d, win, gc, ax + 12, gui_base_row(cy, fs), dt.items[i], t.list_sel_text, fs);
|
||||
} else {
|
||||
gui_text(d, win, gc, ax + 12, gui_base_row(cy, fs), dt.items[i], t.text, fs);
|
||||
}
|
||||
cy += ROW;
|
||||
}
|
||||
w.render_all(d, win, gc, ax + 2, ay + 2 - dt.scroll, vp, ov);
|
||||
}
|
||||
set_clip(d, gc, clip);
|
||||
|
||||
int chh = w.content_height();
|
||||
if (chh > dt.height) {
|
||||
int track = dt.height - 8;
|
||||
int thumb = std::max(30, track * dt.height / chh);
|
||||
int ty = ay + 4 + (int)((float)dt.scroll / (chh - dt.height) * (track - thumb));
|
||||
XSetForeground(d, gc, mixc(t.border, t.border_h, w.hover_a));
|
||||
x_fill_round(d, win, gc, ax + dt.width - 12, ty, 8, thumb, 4);
|
||||
}
|
||||
}
|
||||
|
||||
void Widget::draw_popup(Display* d, Drawable win, GC gc, const XRectangle& clip) const {
|
||||
const auto& t = theme();
|
||||
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, t.panel_bg);
|
||||
x_fill_round(d, win, gc, ax, ay, data.width, std::max(hgt, t.radius * 2), t.radius);
|
||||
XSetForeground(d, gc, t.border);
|
||||
x_draw_round(d, win, gc, ax, ay, data.width, hgt, t.radius);
|
||||
int cy = ay;
|
||||
int fs = t.font_size;
|
||||
for (size_t i = 0; i < data.items.size(); i++) {
|
||||
if ((int)i == data.selected_index) {
|
||||
XSetForeground(d, gc, t.list_sel);
|
||||
x_fill_round(d, win, gc, ax + 4, cy, data.width - 8, ROW, t.radius_sm);
|
||||
gui_text(d, win, gc, ax + 12, gui_base_row(cy, fs), data.items[i], t.list_sel_text, fs);
|
||||
} else {
|
||||
gui_text(d, win, gc, ax + 12, gui_base_row(cy, fs), data.items[i], t.text, fs);
|
||||
}
|
||||
cy += ROW;
|
||||
}
|
||||
for (auto& c : widgets) { c->draw(d, win, gc, ax + 2, cy, vp, nullptr); cy += c->data.height + 4; }
|
||||
set_clip(d, gc, clip);
|
||||
}
|
||||
}
|
||||
102
gui/widgets/draw_toggles.cpp
Normal file
102
gui/widgets/draw_toggles.cpp
Normal file
@ -0,0 +1,102 @@
|
||||
#include "widget.h"
|
||||
namespace gui {
|
||||
void wdraw::toggles(Display* d, Drawable win, GC gc, const Widget& w, int ax, int ay) {
|
||||
const auto& t = theme();
|
||||
const auto& dt = w.data;
|
||||
int fs = dt.font_size ? dt.font_size : t.font_size;
|
||||
float ce = ease(w.check_a);
|
||||
|
||||
switch (dt.type) {
|
||||
case WidgetType::CHECKBOX: {
|
||||
int bs = std::min(dt.height - 16, 40);
|
||||
int bx = ax + 8, by = ay + (dt.height - bs) / 2;
|
||||
|
||||
// заливка: фон -> акцент (правильный цвет, без hover-мерцания)
|
||||
uint32_t fill = (ce > 0.5f) ? t.accent : C(dt.bg_color, t.input_bg);
|
||||
XSetForeground(d, gc, fill);
|
||||
x_fill_round(d, win, gc, bx, by, bs, bs, t.radius_sm);
|
||||
|
||||
// рамка: СТАТИЧНЫЙ цвет (не зависит от hover_a) -> не мерцает
|
||||
XSetForeground(d, gc, t.border);
|
||||
x_draw_round(d, win, gc, bx, by, bs, bs, t.radius_sm);
|
||||
|
||||
// галочка: плавное появление через линии (не мерцает)
|
||||
if (ce > 0.02f) {
|
||||
float p1 = std::min(1.f, ce * 2), p2 = std::max(0.f, ce * 2 - 1);
|
||||
int x1 = bx + bs * 20 / 100, y1 = by + bs * 55 / 100;
|
||||
int xm = bx + bs * 42 / 100, ym = by + bs * 78 / 100;
|
||||
int x2 = bx + bs * 82 / 100, y2 = by + bs * 22 / 100;
|
||||
XSetForeground(d, gc, t.on_accent);
|
||||
XSetLineAttributes(d, gc, 4, LineSolid, CapRound, JoinRound);
|
||||
if (p1 > 0) XDrawLine(d, win, gc, x1, y1, x1 + (int)((xm - x1) * p1), y1 + (int)((ym - y1) * p1));
|
||||
if (p2 > 0) XDrawLine(d, win, gc, xm, ym, xm + (int)((x2 - xm) * p2), ym + (int)((y2 - ym) * p2));
|
||||
XSetLineAttributes(d, gc, 1, LineSolid, CapNotLast, JoinMiter);
|
||||
}
|
||||
gui_text(d, win, gc, bx + bs + 16, gui_base_c(ay, dt.height, fs), dt.text, t.text, fs);
|
||||
break;
|
||||
}
|
||||
case WidgetType::RADIO: {
|
||||
int r = std::min(dt.height - 16, 40) / 2;
|
||||
int cx = ax + r + 8, cy = ay + dt.height / 2;
|
||||
|
||||
// кольцо: статичный цвет (checked ? accent : border), без ce/hover_a -> не мерцает
|
||||
XSetForeground(d, gc, dt.checked ? t.accent : t.border);
|
||||
XDrawArc(d, win, gc, cx - r, cy - r, r * 2, r * 2, 0, 360 * 64);
|
||||
|
||||
// точка: плавное появление (полный круг, не четверти -> не мерцает как углы)
|
||||
int ir = (int)((r - 8) * ce);
|
||||
if (ir > 0) {
|
||||
XSetForeground(d, gc, t.accent);
|
||||
XFillArc(d, win, gc, cx - ir, cy - ir, ir * 2, ir * 2, 0, 360 * 64);
|
||||
}
|
||||
gui_text(d, win, gc, cx + r + 16, gui_base_c(ay, dt.height, fs), dt.text, t.text, fs);
|
||||
break;
|
||||
}
|
||||
case WidgetType::SLIDER: {
|
||||
int ty = ay + dt.height / 2 - 4;
|
||||
|
||||
// трек
|
||||
XSetForeground(d, gc, t.track);
|
||||
x_fill_round(d, win, gc, ax, ty, dt.width, 8, 4);
|
||||
|
||||
// заполнение
|
||||
int fw = (int)(dt.width * dt.value);
|
||||
if (fw > 8) {
|
||||
XSetForeground(d, gc, t.accent);
|
||||
x_fill_round(d, win, gc, ax, ty, fw, 8, 4);
|
||||
}
|
||||
|
||||
// ползунок: размер пульсирует от press_a (это ок - круг целиком, не мерцает),
|
||||
// но ЦВЕТ статичный (убрали hover_a из mixc)
|
||||
int kx = ax + (int)(dt.value * (dt.width - 8));
|
||||
int tr = 12 + (int)(4 * w.press_a);
|
||||
XSetForeground(d, gc, t.accent);
|
||||
XFillArc(d, win, gc, kx - tr, ay + dt.height / 2 - tr, tr * 2, tr * 2, 0, 360 * 64);
|
||||
XSetForeground(d, gc, t.on_accent);
|
||||
XFillArc(d, win, gc, kx - 6, ay + dt.height / 2 - 6, 12, 12, 0, 360 * 64);
|
||||
break;
|
||||
}
|
||||
case WidgetType::PROGRESS: {
|
||||
XSetForeground(d, gc, t.prog_bg);
|
||||
x_fill_round(d, win, gc, ax, ay, dt.width, dt.height, t.radius_sm);
|
||||
|
||||
float v = std::clamp(dt.value, 0.f, 1.f);
|
||||
int fw = (int)((dt.width - 4) * v);
|
||||
if (fw > 4) {
|
||||
XSetForeground(d, gc, t.accent);
|
||||
x_fill_round(d, win, gc, ax + 2, ay + 2, std::max(fw, t.radius_sm * 2), dt.height - 4, t.radius_sm);
|
||||
}
|
||||
|
||||
// рамка: статичный цвет
|
||||
XSetForeground(d, gc, t.border);
|
||||
x_draw_round(d, win, gc, ax, ay, dt.width, dt.height, t.radius_sm);
|
||||
|
||||
std::string pct = std::to_string((int)(v * 100)) + "%";
|
||||
gui_text(d, win, gc, ax + dt.width / 2 - gui_text_width(pct, fs) / 2,
|
||||
gui_base_c(ay, dt.height, fs), pct, t.text, fs);
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
}
|
||||
261
gui/widgets/widget.cpp
Normal file
261
gui/widgets/widget.cpp
Normal file
@ -0,0 +1,261 @@
|
||||
#include "widget.h"
|
||||
#include "../window.h"
|
||||
namespace gui {
|
||||
|
||||
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* r = nullptr;
|
||||
visit([&](Widget& w){ if (!r && w.data.id == id) r = &w; });
|
||||
return r;
|
||||
}
|
||||
Widget* combat::find_at(int mx, int my) {
|
||||
Widget* r = nullptr;
|
||||
visit([&](Widget& w){ if (w.contains(mx, my, 0, 0)) r = &w; });
|
||||
return r;
|
||||
}
|
||||
int combat::count_widgets() const {
|
||||
int n = (int)widgets.size();
|
||||
for (auto& w : widgets) n += w->count_widgets();
|
||||
return n;
|
||||
}
|
||||
void combat::tick(float dt) { for (auto& w : widgets) w->tick(dt); }
|
||||
void combat::render_all(Display* d, Drawable 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);
|
||||
}
|
||||
void combat::render_dirty(Display* d, Drawable win, GC gc, int ox, int oy, const DirtyRegion& dirty, OverlayList* ov) const {
|
||||
for (auto& w : widgets) {
|
||||
int ax = w->data.x + ox, ay = w->data.y + oy;
|
||||
if (dirty.intersects(ax, ay, w->data.width, w->data.height))
|
||||
w->draw(d, win, gc, ox, oy, {(short)0,(short)0,(unsigned short)4096,(unsigned short)4096}, ov);
|
||||
}
|
||||
}
|
||||
void combat::mark_all_dirty(DirtyRegion& dirty, int ox, int oy) const {
|
||||
for (auto& w : widgets) {
|
||||
w->mark_dirty(dirty, ox, oy);
|
||||
w->combat::mark_all_dirty(dirty, w->data.x + ox, w->data.y + oy);
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
void Widget::mark_dirty(DirtyRegion& dirty, int ox, int oy) const {
|
||||
dirty.add(data.x + ox, data.y + oy, data.width, data.height + 8);
|
||||
}
|
||||
|
||||
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 + 4;
|
||||
} 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 + 4;
|
||||
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 + 8;
|
||||
}
|
||||
|
||||
void Widget::tick(float dt) {
|
||||
hover_a = approach(hover_a, hovered ? 1.f : 0.f, dt, 10);
|
||||
press_a = approach(press_a, pressed ? 1.f : 0.f, dt, 14);
|
||||
push_a = approach(push_a, pressed ? 1.f : 0.f, dt, 18);
|
||||
check_a = approach(check_a, data.checked ? 1.f : 0.f, dt, 10);
|
||||
if (data.type == WidgetType::DROPDOWN)
|
||||
anim = approach(anim, open_target ? 1.f : 0.f, dt, 10);
|
||||
if (data.type == WidgetType::PROGRESS) {
|
||||
if (data.indeterminate) {
|
||||
phase += dt * 0.9f;
|
||||
data.value = std::fmod(phase, 1.3f) - 0.15f;
|
||||
} else {
|
||||
data.value = approach(data.value, data.target_value, dt, 8);
|
||||
}
|
||||
}
|
||||
combat::tick(dt);
|
||||
}
|
||||
|
||||
|
||||
|
||||
void Widget::draw(Display* d, Drawable win, GC gc, int ox, int oy, const XRectangle& clip, OverlayList* ov) const {
|
||||
int ax = data.x + ox;
|
||||
int ay = data.y + oy + (data.type == WidgetType::SLIDER ? 0 : (int)(ease(push_a) * 6));
|
||||
switch (data.type) {
|
||||
case WidgetType::BUTTON: case WidgetType::LABEL:
|
||||
case WidgetType::INPUT: case WidgetType::SPRITE_BTN:
|
||||
wdraw::basic(d, win, gc, *this, ax, ay, ov); break;
|
||||
case WidgetType::CHECKBOX: case WidgetType::RADIO:
|
||||
case WidgetType::SLIDER: case WidgetType::PROGRESS:
|
||||
wdraw::toggles(d, win, gc, *this, ax, ay); break;
|
||||
case WidgetType::LISTBOX: case WidgetType::SCROLL:
|
||||
case WidgetType::TREE_VIEW: case WidgetType::DROPDOWN: case WidgetType::RAW_PIC:
|
||||
wdraw::lists(d, win, gc, *this, ax, ay, clip, ov); break;
|
||||
}
|
||||
}
|
||||
|
||||
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 + 4;
|
||||
}
|
||||
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 + 8 + depth * 28;
|
||||
if (my >= y && my < y + ROW && mx >= nx && mx < nx + 56) {
|
||||
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 + 60, y); if (r) return r; y += c->data.height + 4; }
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
int y = ay + 4 - 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;
|
||||
pressed = true;
|
||||
EventBus::get().emit("slider.change", {data.id, "", data.value, 0, false});
|
||||
if (data.on_change) data.on_change();
|
||||
return 2;
|
||||
}
|
||||
if (!contains(mx, my, ox, oy)) return 0;
|
||||
switch (data.type) {
|
||||
case WidgetType::BUTTON: case WidgetType::SPRITE_BTN:
|
||||
pressed = true;
|
||||
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() { pressed = false; }
|
||||
|
||||
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 * 40, 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);
|
||||
default:
|
||||
if (contains(mx, my, ox, oy)) return process_wheel(delta, mx, my, ax, ay);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
111
gui/widgets/widget.h
Normal file
111
gui/widgets/widget.h
Normal file
@ -0,0 +1,111 @@
|
||||
#pragma once
|
||||
#include "../core/common.h"
|
||||
#include "../core/event_bus.h"
|
||||
#include "../core/raw_picture.h"
|
||||
#include "../core/font_manager.h"
|
||||
#include "../core/theme.h"
|
||||
|
||||
namespace gui {
|
||||
class window; class page; struct Widget;
|
||||
|
||||
using callback_void = std::function<void()>;
|
||||
using callback_page = std::function<void(page&)>;
|
||||
using callback_window = std::function<void(window&)>;
|
||||
using OverlayList = std::vector<const Widget*>;
|
||||
|
||||
inline uint32_t C(unsigned long ov, uint32_t th) { return ov ? (uint32_t)ov : th; }
|
||||
|
||||
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, id;
|
||||
unsigned long bg_color = 0, fg_color = 0, frame_color = 0;
|
||||
int x = 0, y = 0, width = 200, height = 60;
|
||||
int font_size = 0;
|
||||
bool primary = false;
|
||||
bool checked = false;
|
||||
float value = 0, target_value = 0;
|
||||
int selected_index = -1, scroll = 0;
|
||||
bool expanded = false, indeterminate = false;
|
||||
std::vector<std::string> items;
|
||||
std::shared_ptr<RawPicture> picture;
|
||||
callback_void on_change;
|
||||
callback_page on_page_click;
|
||||
callback_window on_window_click;
|
||||
};
|
||||
|
||||
class DirtyRegion;
|
||||
|
||||
namespace wdraw {
|
||||
void basic(Display*, Drawable, GC, const Widget&, int ax, int ay, OverlayList* ov);
|
||||
void toggles(Display*, Drawable, GC, const Widget&, int ax, int ay);
|
||||
void lists(Display*, Drawable, GC, const Widget&, int ax, int ay,
|
||||
const XRectangle& clip, OverlayList* ov);
|
||||
}
|
||||
|
||||
class combat {
|
||||
friend struct Widget;
|
||||
friend void wdraw::basic(Display*, Drawable, GC, const Widget&, int, int, OverlayList*);
|
||||
friend void wdraw::toggles(Display*, Drawable, GC, const Widget&, int, int);
|
||||
friend void wdraw::lists(Display*, Drawable, GC, const Widget&, int, int,
|
||||
const XRectangle&, OverlayList*);
|
||||
protected:
|
||||
std::vector<std::unique_ptr<Widget>> 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<void(Widget&)>& fn);
|
||||
Widget* find(const std::string& id);
|
||||
Widget* find_at(int mx, int my);
|
||||
int count_widgets() const;
|
||||
void tick(float dt);
|
||||
void render_all(Display*, Drawable, GC, int ox, int oy, const XRectangle& clip, OverlayList* ov) const;
|
||||
void render_dirty(Display*, Drawable, GC, int ox, int oy, const DirtyRegion&, OverlayList* ov) const;
|
||||
void mark_all_dirty(DirtyRegion&, int ox, int oy) 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 hover_a = 0, press_a = 0, check_a = 0, push_a = 0;
|
||||
float anim = 0, phase = 0;
|
||||
bool hovered = false, pressed = false, open_target = false;
|
||||
int cap_ox = 0, cap_oy = 0;
|
||||
bool focused = false;
|
||||
bool cursor_visible = true;
|
||||
uint32_t cursor_timer = 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*, Drawable, GC, int ox, int oy, const XRectangle& clip, OverlayList* ov) const;
|
||||
void draw_popup(Display*, Drawable, 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;
|
||||
void mark_dirty(DirtyRegion&, int ox, int oy) const;
|
||||
};
|
||||
|
||||
class page : public combat {
|
||||
public:
|
||||
bool active = false;
|
||||
int x = 0, y = 0, width = 1600, height = 900;
|
||||
};
|
||||
}
|
||||
409
gui/window.cpp
Normal file
409
gui/window.cpp
Normal file
@ -0,0 +1,409 @@
|
||||
#include "window.h"
|
||||
#include <unistd.h>
|
||||
#include <sstream>
|
||||
#include <chrono>
|
||||
namespace gui {
|
||||
void font_init_fallback(Display*, GC);
|
||||
|
||||
window::window(std::string title) {
|
||||
X11display = XOpenDisplay(NULL);
|
||||
if (!X11display) { std::cerr << "Cannot open display\n"; should_close = true; return; }
|
||||
int s = DefaultScreen(X11display);
|
||||
X11window = XCreateSimpleWindow(X11display, RootWindow(X11display, s), 10, 10,
|
||||
win_width, win_height, 1, BlackPixel(X11display, s), theme().window_bg);
|
||||
XStoreName(X11display, X11window, title.c_str());
|
||||
XSelectInput(X11display, X11window, ExposureMask | KeyPressMask | StructureNotifyMask |
|
||||
ButtonPressMask | ButtonReleaseMask | ButtonMotionMask | PointerMotionMask);
|
||||
wm_delete = XInternAtom(X11display, "WM_DELETE_WINDOW", False);
|
||||
XSetWMProtocols(X11display, X11window, &wm_delete, 1);
|
||||
XMapWindow(X11display, X11window);
|
||||
gc = XCreateGC(X11display, X11window, 0, NULL);
|
||||
font_init_fallback(X11display, gc);
|
||||
|
||||
auto& fm = FontManager::get();
|
||||
const char* paths[] = {"SGr-Iosevka-Regular.ttc", "./SGr-Iosevka-Regular.ttc",
|
||||
"Terminus.ttf", "./Terminus.ttf", nullptr};
|
||||
for (int i = 0; paths[i] && !fm.available(); i++) fm.load(paths[i]);
|
||||
fm.set_default_size(theme().font_size);
|
||||
|
||||
XGCValues vals; vals.function = GXxor; vals.foreground = 0x00FF00;
|
||||
xor_gc = XCreateGC(X11display, X11window, GCFunction | GCForeground, &vals);
|
||||
ensure_backbuffer();
|
||||
|
||||
EventBus::get().set_global_logger([this](const std::string& e, const EventData& d) {
|
||||
std::ostringstream ss;
|
||||
ss << "[EVENT] " << e;
|
||||
if (!d.source_id.empty()) ss << " src=" << d.source_id;
|
||||
if (!d.text.empty()) ss << " txt=" << d.text;
|
||||
if (d.index >= 0) ss << " idx=" << d.index;
|
||||
debug_info.last_event = ss.str();
|
||||
debug_info.add_log(ss.str());
|
||||
if (debug_info.log_to_console) std::cout << ss.str() << "\n";
|
||||
});
|
||||
std::cout << "[HOTKEYS] F1 overlay | F2 dirty | F3 clicks | F4 bounds | F5 log | F6 theme\n";
|
||||
XFlush(X11display);
|
||||
}
|
||||
window::~window() {
|
||||
if (backbuffer) XFreePixmap(X11display, backbuffer);
|
||||
if (xor_gc) XFreeGC(X11display, xor_gc);
|
||||
if (gc) XFreeGC(X11display, gc);
|
||||
if (X11window) XDestroyWindow(X11display, X11window);
|
||||
if (X11display) XCloseDisplay(X11display);
|
||||
}
|
||||
|
||||
void window::set_font(const std::string& p) { if (FontManager::get().load(p)) mark_all_dirty(); }
|
||||
void window::set_font_size(int px) { FontManager::get().set_default_size(px); mark_all_dirty(); }
|
||||
void window::set_icon(std::shared_ptr<RawPicture> pic) {
|
||||
if (!pic || !X11display) return;
|
||||
int w = pic->width(), h = pic->height();
|
||||
std::vector<unsigned long> data(2 + (size_t)w * h);
|
||||
data[0] = w; data[1] = h;
|
||||
for (int y = 0; y < h; y++)
|
||||
for (int x = 0; x < w; x++) data[2 + (size_t)y * w + x] = 0xFF000000ul | pic->getPixel(x, y);
|
||||
XChangeProperty(X11display, X11window, XInternAtom(X11display, "_NET_WM_ICON", False),
|
||||
XA_CARDINAL, 32, PropModeReplace, (unsigned char*)data.data(), (int)data.size());
|
||||
}
|
||||
void window::set_icon(const std::string& path) {
|
||||
auto p = std::make_shared<RawPicture>();
|
||||
if (p->load(path)) set_icon(p);
|
||||
}
|
||||
void window::set_default_icon() {
|
||||
auto p = std::make_shared<RawPicture>(64, 64, 32);
|
||||
for (int y = 0; y < 64; y++)
|
||||
for (int x = 0; x < 64; x++)
|
||||
p->setPixel(x, y, ((uint32_t)(30 + y) << 16) | ((uint32_t)(90 + x) << 8) | 210);
|
||||
p->fillRoundRect(4, 4, 56, 56, 12, 0x0078D4);
|
||||
p->fillRect(16, 16, 12, 32, 0xFFFFFF);
|
||||
p->fillRect(16, 16, 32, 10, 0xFFFFFF);
|
||||
p->fillRect(16, 28, 24, 8, 0xFFFFFF);
|
||||
p->fillRect(16, 38, 32, 10, 0xFFFFFF);
|
||||
set_icon(p);
|
||||
}
|
||||
|
||||
void window::ensure_backbuffer() {
|
||||
if (backbuffer) XFreePixmap(X11display, backbuffer);
|
||||
backbuffer = XCreatePixmap(X11display, X11window, win_width, win_height,
|
||||
DefaultDepth(X11display, DefaultScreen(X11display)));
|
||||
XSetForeground(X11display, gc, theme().window_bg);
|
||||
XFillRectangle(X11display, backbuffer, gc, 0, 0, win_width, win_height);
|
||||
dirty_region.mark_all(win_width, win_height);
|
||||
}
|
||||
void window::flush_backbuffer(size_t visualize_count) {
|
||||
const auto& rects = dirty_region.get();
|
||||
for (const auto& r : rects)
|
||||
XCopyArea(X11display, backbuffer, X11window, gc, r.x, r.y, r.width, r.height, r.x, r.y);
|
||||
debug_info.partial_updates += (int)rects.size();
|
||||
if (debug_info.show_dirty_rects)
|
||||
for (size_t i = 0; i < visualize_count && i < rects.size(); i++)
|
||||
debug_info.dirty_flashes.push_back({rects[i].x, rects[i].y, rects[i].width, rects[i].height, 0.2f});
|
||||
}
|
||||
|
||||
XRectangle window::overlay_rect() const {
|
||||
auto& fm = FontManager::get();
|
||||
int size = fm.default_size();
|
||||
int line_h = fm.line_height(size) + 8;
|
||||
int panel_w = fm.text_width("Partial updates: 1000000", size) + 40;
|
||||
int panel_h = 19 * line_h + 40;
|
||||
int x = std::max(0, win_width - panel_w - 12);
|
||||
return {(short)x, 12, (unsigned short)panel_w, (unsigned short)std::min(panel_h, win_height - 24)};
|
||||
}
|
||||
|
||||
void window::mark_widget_changed(page& pg, Widget* w) {
|
||||
if (!w) return;
|
||||
w->mark_dirty(dirty_region, 0, 0);
|
||||
if (w->data.type == WidgetType::DROPDOWN)
|
||||
dirty_region.add(w->data.x, w->data.y + w->data.height, w->data.width, w->content_height());
|
||||
if (w->data.type == WidgetType::RADIO)
|
||||
pg.visit([&](Widget& x) {
|
||||
if (x.data.type == WidgetType::RADIO && x.data.id == w->data.id)
|
||||
x.mark_dirty(dirty_region, 0, 0);
|
||||
});
|
||||
}
|
||||
|
||||
void window::update() {
|
||||
if (!X11display || should_close) 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();
|
||||
const float min_dt = 1.f / (float)std::max(1, max_fps);
|
||||
if (dt < min_dt) {
|
||||
usleep((useconds_t)((min_dt - dt) * 1e6f));
|
||||
now = std::chrono::steady_clock::now();
|
||||
dt = std::chrono::duration<float>(now - last).count();
|
||||
}
|
||||
last = now;
|
||||
|
||||
while (XPending(X11display) > 0) { XEvent ev; XNextEvent(X11display, &ev); handle_event(ev); }
|
||||
|
||||
page* cur_pg = current_page_name.empty() ? nullptr : pages[current_page_name].get();
|
||||
if (cur_pg) cur_pg->tick(dt);
|
||||
|
||||
if (cur_pg) cur_pg->visit([&](Widget& w) {
|
||||
bool animating =
|
||||
std::abs(w.hover_a - (w.hovered ? 1.f : 0.f)) > 0.001f ||
|
||||
std::abs(w.press_a - (w.pressed ? 1.f : 0.f)) > 0.001f ||
|
||||
std::abs(w.push_a - (w.pressed ? 1.f : 0.f)) > 0.001f ||
|
||||
std::abs(w.check_a - (w.data.checked ? 1.f : 0.f)) > 0.001f;
|
||||
if (w.data.type == WidgetType::DROPDOWN && std::abs(w.anim - (w.open_target ? 1.f : 0.f)) > 0.001f) {
|
||||
animating = true;
|
||||
dirty_region.add(w.data.x, w.data.y + w.data.height, w.data.width, w.content_height());
|
||||
}
|
||||
if (w.data.type == WidgetType::PROGRESS &&
|
||||
(w.data.indeterminate || std::abs(w.data.value - w.data.target_value) > 0.001f)) animating = true;
|
||||
if (animating) w.mark_dirty(dirty_region, 0, 0);
|
||||
});
|
||||
|
||||
size_t user_rects = dirty_region.get().size();
|
||||
|
||||
for (auto& m : debug_info.click_markers) { m.ttl -= dt; dirty_region.add(m.x - 24, m.y - 24, 49, 49); }
|
||||
debug_info.click_markers.erase(std::remove_if(debug_info.click_markers.begin(),
|
||||
debug_info.click_markers.end(), [](auto& m){ return m.ttl <= 0; }), debug_info.click_markers.end());
|
||||
for (auto& f : debug_info.dirty_flashes) { f.ttl -= dt; dirty_region.add(f.x, f.y, f.w, f.h); }
|
||||
debug_info.dirty_flashes.erase(std::remove_if(debug_info.dirty_flashes.begin(),
|
||||
debug_info.dirty_flashes.end(), [](auto& f){ return f.ttl <= 0; }), debug_info.dirty_flashes.end());
|
||||
|
||||
if (debug_info.show_debug_overlay) {
|
||||
overlay_timer += dt;
|
||||
if (overlay_timer >= 0.1f) { dirty_region.add(overlay_rect()); overlay_timer = 0; }
|
||||
}
|
||||
|
||||
debug_info.widget_count = cur_pg ? cur_pg->count_widgets() : 0;
|
||||
|
||||
if (dirty_region.is_dirty()) {
|
||||
XSetForeground(X11display, gc, theme().window_bg);
|
||||
for (const auto& r : dirty_region.get())
|
||||
XFillRectangle(X11display, backbuffer, gc, r.x, r.y, r.width, r.height);
|
||||
draw_tabs();
|
||||
draw_active_page();
|
||||
|
||||
if (debug_info.show_dirty_rects)
|
||||
for (const auto& f : debug_info.dirty_flashes)
|
||||
if (f.ttl > 0) XFillRectangle(X11display, backbuffer, xor_gc, f.x, f.y, f.w, f.h);
|
||||
if (debug_info.show_click_markers) {
|
||||
XSetForeground(X11display, gc, 0xFF4444);
|
||||
XSetLineAttributes(X11display, gc, 3, LineSolid, CapRound, JoinRound);
|
||||
for (const auto& m : debug_info.click_markers) if (m.ttl > 0) {
|
||||
XDrawLine(X11display, backbuffer, gc, m.x - 16, m.y - 16, m.x + 16, m.y + 16);
|
||||
XDrawLine(X11display, backbuffer, gc, m.x - 16, m.y + 16, m.x + 16, m.y - 16);
|
||||
XDrawArc(X11display, backbuffer, gc, m.x - 20, m.y - 20, 40, 40, 0, 360 * 64);
|
||||
}
|
||||
XSetLineAttributes(X11display, gc, 1, LineSolid, CapNotLast, JoinMiter);
|
||||
}
|
||||
if (debug_info.show_debug_overlay) draw_debug_overlay();
|
||||
flush_backbuffer(user_rects);
|
||||
dirty_region.clear();
|
||||
debug_info.frame_count++;
|
||||
debug_info.fps_time += dt;
|
||||
if (debug_info.fps_time >= 0.5f) {
|
||||
debug_info.fps = (int)(debug_info.frame_count / debug_info.fps_time);
|
||||
debug_info.frame_count = 0; debug_info.fps_time = 0;
|
||||
}
|
||||
} else usleep(10000);
|
||||
XFlush(X11display);
|
||||
}
|
||||
|
||||
void window::draw_tabs() {
|
||||
const auto& t = theme();
|
||||
for (const auto& b : tab_buttons) {
|
||||
if (!dirty_region.intersects(b.x, b.y, b.width, b.height)) continue;
|
||||
bool active = (b.id == current_page_name);
|
||||
XSetForeground(X11display, gc, active ? t.tab_active : t.tab_bg);
|
||||
x_fill_round(X11display, backbuffer, gc, b.x, b.y, b.width, b.height, t.radius);
|
||||
if (active) {
|
||||
XSetForeground(X11display, gc, t.accent);
|
||||
XFillRectangle(X11display, backbuffer, gc, b.x + t.radius, b.y + b.height - 5,
|
||||
b.width - 2 * t.radius, 5);
|
||||
}
|
||||
gui_text(X11display, backbuffer, gc, b.x + 16, gui_base_c(b.y, b.height, t.font_size),
|
||||
b.text, active ? t.text : t.text_dim, t.font_size);
|
||||
}
|
||||
XSetForeground(X11display, gc, t.border);
|
||||
XDrawLine(X11display, backbuffer, gc, 340, 0, 340, win_height);
|
||||
}
|
||||
void window::draw_active_page() {
|
||||
if (current_page_name.empty()) return;
|
||||
auto it = pages.find(current_page_name);
|
||||
if (it == pages.end()) return;
|
||||
XRectangle full{0, 160, (unsigned short)win_width, (unsigned short)(win_height - 160)};
|
||||
OverlayList ov;
|
||||
it->second->render_dirty(X11display, backbuffer, gc, 0, 0, dirty_region, &ov);
|
||||
for (auto* w : ov) w->draw_popup(X11display, backbuffer, gc, full);
|
||||
if (debug_info.show_widget_bounds) {
|
||||
XSetForeground(X11display, gc, 0xFF00FF);
|
||||
it->second->visit([&](Widget& w) {
|
||||
if (dirty_region.intersects(w.data.x, w.data.y, w.data.width, w.data.height))
|
||||
XDrawRectangle(X11display, backbuffer, gc, w.data.x, w.data.y, w.data.width, w.data.height);
|
||||
});
|
||||
}
|
||||
}
|
||||
void window::draw_debug_overlay() {
|
||||
auto& fm = FontManager::get();
|
||||
int size = fm.default_size();
|
||||
int line_h = fm.line_height(size) + 8, pad = 20;
|
||||
XRectangle r = overlay_rect();
|
||||
XSetForeground(X11display, gc, 0x000000);
|
||||
XFillRectangle(X11display, backbuffer, gc, r.x, r.y, r.width, r.height);
|
||||
XSetForeground(X11display, gc, 0x00CC00);
|
||||
XDrawRectangle(X11display, backbuffer, gc, r.x, r.y, r.width, r.height);
|
||||
int ty = r.y + pad;
|
||||
auto line = [&](const std::string& s, uint32_t c) {
|
||||
gui_text(X11display, backbuffer, gc, r.x + pad, ty + fm.ascent(size), s, c, size);
|
||||
ty += line_h;
|
||||
};
|
||||
line("=== DEBUG OVERLAY ===", 0x00FF00);
|
||||
line("FPS: " + std::to_string(debug_info.fps),
|
||||
debug_info.fps >= 50 ? 0x00FF00 : debug_info.fps >= 30 ? 0xFFFF00 : 0xFF4444);
|
||||
line("Widgets: " + std::to_string(debug_info.widget_count), 0xCCCCCC);
|
||||
line("Partial updates: " + std::to_string(debug_info.partial_updates), 0xCCCCCC);
|
||||
line("Mouse: " + std::to_string(debug_info.mouse_x) + ", " + std::to_string(debug_info.mouse_y), 0xCCCCCC);
|
||||
line(std::string("[F1] Overlay: ") + (debug_info.show_debug_overlay ? "ON" : "OFF"), debug_info.show_debug_overlay ? 0x00FF00 : 0x888888);
|
||||
line(std::string("[F2] Dirty rects: ") + (debug_info.show_dirty_rects ? "ON" : "OFF"), debug_info.show_dirty_rects ? 0x00FF00 : 0x888888);
|
||||
line(std::string("[F3] Click marks: ") + (debug_info.show_click_markers ? "ON" : "OFF"), debug_info.show_click_markers ? 0x00FF00 : 0x888888);
|
||||
line(std::string("[F4] Bounds: ") + (debug_info.show_widget_bounds ? "ON" : "OFF"), debug_info.show_widget_bounds ? 0x00FF00 : 0x888888);
|
||||
line(std::string("[F5] Console: ") + (debug_info.log_to_console ? "ON" : "OFF"), debug_info.log_to_console ? 0x00FF00 : 0x888888);
|
||||
line(std::string("[F6] Theme: ") + (theme().style == ThemeStyle::WIN11 ? "WIN11" : "TERMINAL"), 0x4CC2FF);
|
||||
int count = 0;
|
||||
for (auto it = debug_info.log.rbegin(); it != debug_info.log.rend() && count < 5; ++it, ++count) {
|
||||
std::string s = *it;
|
||||
size_t maxc = (size_t)((r.width - pad * 2) / std::max(1, fm.text_width("M", size)));
|
||||
if (s.size() > maxc) s = s.substr(0, maxc - 3) + "...";
|
||||
line(s, 0xAAAAAA);
|
||||
}
|
||||
}
|
||||
|
||||
void window::handle_event(const XEvent& ev) {
|
||||
if (ev.type == ClientMessage && (Atom)ev.xclient.data.l[0] == wm_delete) { should_close = true; return; }
|
||||
if (ev.type == ConfigureNotify) {
|
||||
if (ev.xconfigure.width != win_width || ev.xconfigure.height != win_height) {
|
||||
win_width = ev.xconfigure.width; win_height = ev.xconfigure.height;
|
||||
ensure_backbuffer();
|
||||
}
|
||||
}
|
||||
else if (ev.type == MotionNotify) {
|
||||
debug_info.mouse_x = ev.xmotion.x; debug_info.mouse_y = ev.xmotion.y;
|
||||
if (captured) {
|
||||
captured->mark_dirty(dirty_region, 0, 0);
|
||||
captured->handle_drag(ev.xmotion.x, ev.xmotion.y);
|
||||
captured->mark_dirty(dirty_region, 0, 0);
|
||||
} else if (!current_page_name.empty()) {
|
||||
Widget* h = pages[current_page_name]->find_at(ev.xmotion.x, ev.xmotion.y);
|
||||
if (h != hovered) {
|
||||
if (hovered) { hovered->hovered = false; hovered->mark_dirty(dirty_region, 0, 0); }
|
||||
hovered = h;
|
||||
if (hovered) { hovered->hovered = true; hovered->mark_dirty(dirty_region, 0, 0); }
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (ev.type == ButtonPress && ev.xbutton.button == Button1) {
|
||||
int mx = ev.xbutton.x, my = ev.xbutton.y;
|
||||
if (debug_info.show_click_markers) debug_info.click_markers.push_back({mx, my, 0.5f, 0xFF4444});
|
||||
for (auto& tab : tab_buttons)
|
||||
if (mx >= tab.x && mx <= tab.x + tab.width && my >= tab.y && my <= tab.y + tab.height) {
|
||||
dirty_region.add(tab.x, tab.y, tab.width, 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);
|
||||
if (captured) { captured->mark_dirty(dirty_region, 0, 0); hovered = captured; pressed_w = captured; }
|
||||
} else if (r > 0) {
|
||||
Widget* w = pg->find_at(mx, my);
|
||||
mark_widget_changed(*pg, w);
|
||||
pressed_w = w;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (ev.type == ButtonRelease) {
|
||||
if (captured) {
|
||||
captured->mark_dirty(dirty_region, 0, 0);
|
||||
captured->handle_release();
|
||||
captured = nullptr;
|
||||
}
|
||||
if (!current_page_name.empty()) {
|
||||
pages[current_page_name]->visit([&](Widget& w) {
|
||||
if (w.pressed) {
|
||||
w.pressed = false;
|
||||
// если виджет внутри SCROLL/LISTBOX/TREE - помечаем ВЕСЬ контейнер
|
||||
// (простой способ: помечаем все виджеты с запасом)
|
||||
w.mark_dirty(dirty_region, 0, 0);
|
||||
}
|
||||
});
|
||||
// ДОПОЛНИТЕЛЬНО: помечаем все ScrollView/Listbox/TreeView целиком
|
||||
pages[current_page_name]->visit([&](Widget& w) {
|
||||
if (w.data.type == WidgetType::SCROLL ||
|
||||
w.data.type == WidgetType::LISTBOX ||
|
||||
w.data.type == WidgetType::TREE_VIEW) {
|
||||
dirty_region.add(w.data.x, w.data.y, w.data.width, w.data.height);
|
||||
}
|
||||
});
|
||||
}
|
||||
pressed_w = nullptr;
|
||||
}
|
||||
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()) {
|
||||
auto& pg = pages[current_page_name];
|
||||
if (pg->process_wheel(delta, ev.xbutton.x, ev.xbutton.y, 0, 0))
|
||||
if (Widget* w = pg->find_at(ev.xbutton.x, ev.xbutton.y)) w->mark_dirty(dirty_region, 0, 0);
|
||||
}
|
||||
}
|
||||
else if (ev.type == KeyPress) {
|
||||
char buf[16]; KeySym ks;
|
||||
int len = XLookupString((XKeyEvent*)&ev, buf, sizeof(buf), &ks, nullptr);
|
||||
if (ks >= XK_F1 && ks <= XK_F5) { handle_key_debug(ks); return; }
|
||||
if (on_key) on_key(ks);
|
||||
if (ks >= XK_F6 && ks <= XK_F12) return;
|
||||
if (focused_input) {
|
||||
focused_input->mark_dirty(dirty_region, 0, 0);
|
||||
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];
|
||||
if (focused_input) focused_input->mark_dirty(dirty_region, 0, 0);
|
||||
}
|
||||
}
|
||||
else if (ev.type == Expose) {
|
||||
dirty_region.add(ev.xexpose.x, ev.xexpose.y, ev.xexpose.width, ev.xexpose.height);
|
||||
}
|
||||
}
|
||||
|
||||
void window::handle_key_debug(KeySym ks) {
|
||||
switch (ks) {
|
||||
case XK_F1: debug_info.show_debug_overlay = !debug_info.show_debug_overlay; mark_all_dirty(); break;
|
||||
case XK_F2: debug_info.show_dirty_rects = !debug_info.show_dirty_rects; break;
|
||||
case XK_F3: debug_info.show_click_markers = !debug_info.show_click_markers; debug_info.click_markers.clear(); break;
|
||||
case XK_F4: debug_info.show_widget_bounds = !debug_info.show_widget_bounds; mark_all_dirty(); break;
|
||||
case XK_F5: debug_info.log_to_console = !debug_info.log_to_console; break;
|
||||
}
|
||||
}
|
||||
|
||||
void window::addTAB_BUTTON(WidgetData a) {
|
||||
size_t i = tab_buttons.size();
|
||||
a.x = 20; a.y = 20 + (int)i * 68; a.width = 300; a.height = 60;
|
||||
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]);
|
||||
mark_all_dirty();
|
||||
}
|
||||
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;
|
||||
hovered = nullptr;
|
||||
mark_all_dirty();
|
||||
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;
|
||||
}
|
||||
}
|
||||
62
gui/window.h
Normal file
62
gui/window.h
Normal file
@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
#include "widgets/widget.h"
|
||||
namespace gui {
|
||||
class window {
|
||||
private:
|
||||
Display* X11display = nullptr;
|
||||
Window X11window = 0;
|
||||
GC gc = nullptr, xor_gc = nullptr;
|
||||
Pixmap backbuffer = 0;
|
||||
int win_width = 1600, win_height = 900;
|
||||
std::vector<WidgetData> tab_buttons;
|
||||
std::unordered_map<std::string, std::unique_ptr<page>> pages;
|
||||
std::string current_page_name;
|
||||
Widget *captured = nullptr, *focused_input = nullptr, *hovered = nullptr, *pressed_w = nullptr;
|
||||
DirtyRegion dirty_region;
|
||||
DebugInfo debug_info;
|
||||
bool should_close = false;
|
||||
int max_fps = 240;
|
||||
float overlay_timer = 0;
|
||||
Atom wm_delete = 0;
|
||||
|
||||
void draw_tabs();
|
||||
void draw_active_page();
|
||||
void draw_debug_overlay();
|
||||
void handle_event(const XEvent& ev);
|
||||
void handle_key_debug(KeySym ks);
|
||||
void ensure_backbuffer();
|
||||
void flush_backbuffer(size_t visualize_count);
|
||||
XRectangle overlay_rect() const;
|
||||
void mark_widget_changed(page& pg, Widget* w);
|
||||
|
||||
public:
|
||||
std::function<void(KeySym)> on_key;
|
||||
|
||||
explicit window(std::string title);
|
||||
~window();
|
||||
window(const window&) = delete;
|
||||
window& operator=(const window&) = delete;
|
||||
|
||||
Display* display() const { return X11display; }
|
||||
void update();
|
||||
bool running() const { return !should_close; }
|
||||
|
||||
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; }
|
||||
|
||||
void set_font(const std::string& path);
|
||||
void set_font_size(int px);
|
||||
void set_icon(const std::string& path);
|
||||
void set_icon(std::shared_ptr<RawPicture> pic);
|
||||
void set_default_icon();
|
||||
void set_max_fps(int f) { max_fps = f; }
|
||||
|
||||
DebugInfo& debug() { return debug_info; }
|
||||
void mark_dirty(int x, int y, int w, int h) { dirty_region.add(x, y, w, h); }
|
||||
void mark_all_dirty() { dirty_region.mark_all(win_width, win_height); }
|
||||
};
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user