lacteApp
C++17 service for Lacte hardware
Loading...
Searching...
No Matches
TinyGui.hpp
Go to the documentation of this file.
1#pragma once
2#ifdef __linux__
3
4#include <fcntl.h>
5#include <linux/fb.h>
6#include <linux/input.h>
7#include <sys/ioctl.h>
8#include <sys/mman.h>
9#include <unistd.h>
10
11#include <cerrno>
12#include <cstdint>
13#include <memory>
14#include <string>
15#include <vector>
16
17#define NK_INCLUDE_FIXED_TYPES
18#define NK_INCLUDE_STANDARD_IO
19#define NK_INCLUDE_STANDARD_VARARGS
20#define NK_INCLUDE_DEFAULT_ALLOCATOR
21#define NK_INCLUDE_VERTEX_BUFFER_OUTPUT
22#define NK_INCLUDE_FONT_BAKING
23#define NK_INCLUDE_DEFAULT_FONT
24
25#include <nuklear.h>
26
27#include <atomic>
28#include <cstring>
29#include <mutex>
30#include <thread>
31
32namespace tinygui {
33
35inline constexpr unsigned K_FB_REQUIRED_BITS_PER_PIXEL = 32;
36
38inline constexpr std::uint32_t K_DEFAULT_FRAME_TIME_MS = 16;
39
44struct TouchCalibration {
45 float scale_x{1.0f};
46 float scale_y{1.0f};
47 bool valid{false};
48};
49
57[[nodiscard]] auto detect_touch_calibration(const std::string& device_path)
58 -> TouchCalibration;
59
61struct Point {
62 int x{0};
63 int y{0};
64};
65
67struct Size {
68 int width{0};
69 int height{0};
70};
71
73struct Rect {
74 int x{0};
75 int y{0};
76 int w{0};
77 int h{0};
78
79 [[nodiscard]] auto contains(const Point point) const noexcept -> bool {
80 return point.x >= x && point.y >= y && point.x < x + w && point.y < y + h;
81 }
82};
83
86struct FramebufferView {
87 std::uint32_t* pixels{nullptr};
88 int width{0};
89 int height{0};
90 int stride{0}; // в пикселях
91 int fb_desc{-1};
92
93 FramebufferView() = default;
94
95 // Нельзя копировать view, чтобы не было двойного закрытия ресурса.
96 FramebufferView(const FramebufferView&) = delete;
97 auto operator=(const FramebufferView&) -> FramebufferView& = delete;
98
99 // Разрешаем перемещение.
100 FramebufferView(FramebufferView&& other) noexcept
101 : pixels(other.pixels),
102 width(other.width),
103 height(other.height),
104 stride(other.stride),
105 fb_desc(other.fb_desc) {
106 other.pixels = nullptr;
107 other.width = other.height = other.stride = 0;
108 other.fb_desc = -1;
109 }
110
111 auto operator=(FramebufferView&& other) noexcept -> FramebufferView& {
112 if (this != &other) {
113 // Освобождаем текущий ресурс.
114 if (pixels != nullptr && fb_desc >= 0) {
115 // Размер отображения нам тут неизвестен, поэтому предполагаем,
116 // что вызывающий код перед уничтожением вызовет munmap/close
117 // только через деструктор. Для простоты используем close только
118 // по дескриптору, а munmap будем делать в деструкторе.
119 }
120 pixels = other.pixels;
121 width = other.width;
122 height = other.height;
123 stride = other.stride;
124 fb_desc = other.fb_desc;
125
126 other.pixels = nullptr;
127 other.width = other.height = other.stride = 0;
128 other.fb_desc = -1;
129 }
130 return *this;
131 }
132
135 explicit FramebufferView(const std::string& path) {
136 const char* fbdev = path.c_str();
137 fb_desc = open(fbdev, O_RDWR);
138 if (fb_desc < 0) {
139 perror("open fb");
140 return;
141 }
142
143 fb_var_screeninfo vinfo{};
144 if (ioctl(fb_desc, FBIOGET_VSCREENINFO, &vinfo) < 0) {
145 perror("FBIOGET_VSCREENINFO");
146 return;
147 }
148 // vinfo.xres_virtual = vinfo.xres;
149 // vinfo.yres_virtual = vinfo.yres;
150 // vinfo.nonstd = 0;
151 // vinfo.yres_virtual = vinfo.yres * 2;
152 vinfo.activate = FB_ACTIVATE_NOW | FB_ACTIVATE_FORCE;
153 if (ioctl(fb_desc, FBIOPUT_VSCREENINFO, &vinfo) < 0) {
154 perror("FBIOGET_VSCREENINFO");
155 return;
156 }
157
158 fb_fix_screeninfo finfo{};
159 if (ioctl(fb_desc, FBIOGET_FSCREENINFO, &finfo) < 0) {
160 perror("FBIOGET_FSCREENINFO");
161 return;
162 }
163
164 if (vinfo.bits_per_pixel != K_FB_REQUIRED_BITS_PER_PIXEL) {
165 fprintf(stderr, "Need 32bpp (ARGB8888), got %u\n", vinfo.bits_per_pixel);
166 return;
167 }
168
169 width = static_cast<int>(vinfo.xres);
170 height = static_cast<int>(vinfo.yres);
171 // stride в пикселях: длина строки в байтах / 4 байта на пиксель ARGB8888
172 stride = static_cast<int>(finfo.line_length / 4);
173
174 const size_t screensize = static_cast<size_t>(finfo.line_length) *
175 static_cast<size_t>(vinfo.yres);
176 void* const fbp = mmap(nullptr, screensize, PROT_READ | PROT_WRITE,
177 MAP_SHARED, fb_desc, 0);
178 if (fbp == MAP_FAILED) {
179 perror("mmap");
180 pixels = nullptr;
181 width = height = stride = 0;
182 return;
183 }
184
185 pixels = static_cast<std::uint32_t*>(fbp);
186 }
187 explicit FramebufferView(const char* path)
188 : FramebufferView(std::string(path)) {}
189 [[nodiscard]] auto valid() const noexcept -> bool {
190 return pixels != nullptr && width > 0 && height > 0 && stride >= width;
191 }
192
194 auto put_pixel_unsafe(const int pos_x, const int pos_y,
195 const std::uint32_t argb) const noexcept -> void {
196 pixels[pos_y * stride + pos_x] = argb;
197 }
198
200 auto put_pixel(const int pos_x, const int pos_y,
201 const std::uint32_t argb) const noexcept -> void {
202 if (pos_x < 0 || pos_y < 0 || pos_x >= width || pos_y >= height) {
203 return;
204 }
205 put_pixel_unsafe(pos_x, pos_y, argb);
206 }
207
208 ~FramebufferView() {
209 if (pixels != nullptr && fb_desc >= 0) {
210 // Пытаемся определить размер отображения по текущим параметрам.
211 // Предполагаем, что stride – в пикселях, по 4 байта на пиксель.
212 const size_t screensize =
213 static_cast<size_t>(stride) * static_cast<size_t>(height) * 4;
214 if (screensize > 0) {
215 munmap(pixels, screensize);
216 }
217 close(fb_desc);
218 }
219 }
220};
221
223enum class TouchEventType : std::uint8_t {
224 DOWN,
225 UP,
226 MOVE,
227};
228
230struct TouchEvent {
231 TouchEventType type{};
232 Point position{};
233 std::uint32_t id{0};
234 std::uint64_t time_ms{0};
235};
236
240class ITouchInput {
241 public:
242 virtual ~ITouchInput() = default;
243
246 virtual auto poll(TouchEvent& event) -> bool = 0;
247 virtual auto get_path() -> std::string = 0;
248 virtual auto set_path(const std::string& new_path) -> bool = 0;
249 virtual auto start(std::string path = "") -> bool = 0;
250 virtual auto stop() -> bool = 0;
251};
252
254class EvdevTouchInput final : public ITouchInput {
255 public:
256 auto get_path() -> std::string override { return path_; }
257 auto start(const std::string PATH = "") -> bool override {
258 if (!PATH.empty()) {
259 path_ = PATH;
260 }
261 stop();
262 std::scoped_lock lock(poll_mtx_);
263 fd_ = open(path_.c_str(), O_RDONLY | O_NONBLOCK);
264 if (fd_ < 0) {
265 perror("open evdev");
266 return false;
267 }
268 ioctl(fd_, EVIOCGRAB, 1);
269 return true;
270 }
271 auto stop() -> bool override {
272 std::scoped_lock lock(poll_mtx_);
273 if (fd_ >= 0) {
274 ioctl(fd_, EVIOCGRAB, 0);
275 close(fd_);
276 fd_ = -1;
277 return true;
278 }
279 return false;
280 }
281 auto set_path(const std::string& new_path) -> bool override {
282 stop();
283 path_ = new_path;
284 return true;
285 }
287 explicit EvdevTouchInput(const std::string& path) : fd_(-1) {
288 EvdevTouchInput::set_path(path);
289 }
290
292 explicit EvdevTouchInput(const char* path)
293 : EvdevTouchInput(std::string(path)) {}
294
295 ~EvdevTouchInput() override { stop(); }
296
297 EvdevTouchInput(const EvdevTouchInput&) = delete;
298 auto operator=(const EvdevTouchInput&) -> EvdevTouchInput& = delete;
299
300 EvdevTouchInput(EvdevTouchInput&& other) noexcept
301 : fd_(other.fd_),
302 cur_x_(other.cur_x_),
303 cur_y_(other.cur_y_),
304 touching_(other.touching_) {
305 other.fd_ = -1;
306 other.cur_x_ = other.cur_y_ = 0;
307 other.touching_ = false;
308 }
309
310 auto operator=(EvdevTouchInput&& other) noexcept -> EvdevTouchInput& {
311 if (this != &other) {
312 stop();
313 fd_ = other.fd_;
314 cur_x_ = other.cur_x_;
315 cur_y_ = other.cur_y_;
316 touching_ = other.touching_;
317
318 other.fd_ = -1;
319 other.cur_x_ = other.cur_y_ = 0;
320 other.touching_ = false;
321 }
322 return *this;
323 }
324
325 auto poll(TouchEvent& event) -> bool override {
326 std::scoped_lock lock(poll_mtx_);
327 if (fd_ < 0) {
328 return false;
329 }
330
331 // В рамках одного вызова poll() читаем поток событий до первого SYN_REPORT
332 // и возвращаем максимум одно логическое событие тача.
333 bool coord_changed = false;
334 bool has_type = false;
335 auto next_type = TouchEventType::MOVE;
336
337 for (;;) {
338 input_event raw{};
339 ssize_t n = read(fd_, &raw, sizeof(raw));
340 if (n < 0) {
341 if (errno == EAGAIN || errno == EWOULDBLOCK) {
342 // Нет данных прямо сейчас.
343 return false;
344 }
345 // Другая ошибка — считаем, что события нет.
346 return false;
347 }
348 if (n != static_cast<ssize_t>(sizeof(raw))) {
349 return false;
350 }
351
352 if (raw.type == EV_ABS) {
353 if (raw.code == ABS_X || raw.code == ABS_MT_POSITION_X) {
354 cur_x_ = raw.value;
355 coord_changed = true;
356 } else if (raw.code == ABS_Y || raw.code == ABS_MT_POSITION_Y) {
357 cur_y_ = raw.value;
358 coord_changed = true;
359 }
360 // Пока только накапливаем координаты, не возвращаем событие — ждём
361 // SYN_REPORT.
362 continue;
363 }
364
365 if (raw.type == EV_KEY && raw.code == BTN_TOUCH) {
366 // Фактическое состояние "палец на экране".
367 touching_ = raw.value != 0;
368 has_type = true;
369 next_type = touching_ ? TouchEventType::DOWN : TouchEventType::UP;
370 // Координаты могли ещё не прийти, поэтому не возвращаем событие сразу.
371 continue;
372 }
373
374 if (raw.type == EV_SYN && raw.code == SYN_REPORT) {
375 // "Кадр" завершён: если что‑то поменялось — генерируем событие.
376 if (has_type || (coord_changed && touching_)) {
377 event.type = has_type ? next_type : TouchEventType::MOVE;
378 event.position = {cur_x_, cur_y_};
379 event.id = 0;
380 event.time_ms =
381 static_cast<std::uint64_t>(raw.time.tv_sec) * 1000ULL +
382 static_cast<std::uint64_t>(raw.time.tv_usec) / 1000ULL;
383 return true;
384 }
385
386 // Если никаких значимых изменений не было — читаем дальше.
387 coord_changed = false;
388 has_type = false;
389 }
390
391 // Остальные события игнорируем и читаем дальше.
392 }
393 }
394
395 private:
396 std::mutex poll_mtx_{};
397 std::string path_;
398 int fd_;
399 int cur_x_{0};
400 int cur_y_{0};
401 bool touching_{false};
402};
403
405struct NkFonts {
406 nk_user_font* normal{nullptr};
407 nk_user_font* big{nullptr};
408 nk_user_font* small{nullptr};
409};
410
412struct BackgroundView {
413 const std::uint32_t* pixels{nullptr};
414 int width{0};
415 int height{0};
416 int stride{0}; // в пикселях
417
418 [[nodiscard]] auto valid() const noexcept -> bool {
419 return pixels != nullptr && width > 0 && height > 0 && stride >= width;
420 }
421};
422
424struct GuiResources {
425 NkFonts fonts;
426 BackgroundView background;
427};
428
431class INkWindow {
432 public:
433 virtual ~INkWindow() = default;
434
439 virtual auto build_ui(nk_context& ctx, const GuiResources& resources)
440 -> void = 0;
441
442 virtual auto has_new_data() -> bool = 0;
443};
444
450class TinyGui {
451 public:
452 TinyGui(FramebufferView& fb, ITouchInput& input) noexcept;
453 ~TinyGui();
454
456 auto add_window(std::shared_ptr<INkWindow> window) -> void;
457
459 auto clear_windows() -> void;
460
462 auto step() -> void;
463 auto is_running() const noexcept -> bool { return running_; }
464
467 auto run(std::uint32_t frame_time_ms = K_DEFAULT_FRAME_TIME_MS) -> void;
468
470 auto stop() noexcept -> void;
471
474 auto set_background_color(const std::uint32_t argb) noexcept -> void {
475 background_color_ = argb;
476 }
477
479 auto set_use_vsync(const bool value) noexcept -> void { use_vsync_ = value; }
480
484 auto set_use_background_clear(const bool value) noexcept -> void {
485 use_background_clear_ = value;
486 }
487
488 private:
490 auto init_nuklear() -> void;
491 auto shutdown_nuklear() -> void;
492
494 auto pump_input() -> void;
495
497 auto render_commands() -> void;
498
500 auto clear_background_to(std::uint32_t* dst, int width, int height,
501 int stride) const -> void;
502
504 auto ensure_backbuffer() -> void;
505
507 [[nodiscard]] auto background_view() const -> BackgroundView;
508
509 std::thread thread_;
510 FramebufferView& fb_;
511 ITouchInput& input_;
512 std::mutex start_mutex_;
513 std::vector<std::shared_ptr<INkWindow>> windows_;
514
515 nk_context* ctx_{nullptr};
516
517 // Данные атласа шрифта Nuklear.
518 std::vector<std::uint8_t> atlas_alpha_;
519 int atlas_w_{0};
520 int atlas_h_{0};
521 nk_user_font* font_normal_{nullptr};
522 nk_user_font* font_big_{nullptr};
523 nk_user_font* font_small_{nullptr};
524
525 // Двойная буферизация и сохранённый фон.
526 std::vector<std::uint32_t>
527 backbuffer_;
528 std::vector<std::uint32_t>
529 saved_background_;
530 bool have_saved_background_{false};
531
532 std::atomic_bool running_{false};
533 std::uint32_t background_color_{0xFF202020}; // по умолчанию тёмно-серый фон
534 bool use_vsync_{true};
535 bool use_background_clear_{
536 false};
537
538 int mouse_x_{0};
539 int mouse_y_{0};
540 bool touch_down_{false};
541 std::atomic_bool touch_event_{false};
542
543 // Калибровка сырых координат тачскрина под конкретную панель (ilitek_ts
544 // vs goodix-ts) - см. TinyGui.cpp::TinyGui() и pump_input(). cal_valid_
545 // false означает "координаты не масштабировать" (панель не распознана).
546 float cal_scale_x_{1.0f};
547 float cal_scale_y_{1.0f};
548 bool cal_valid_{false};
549};
550} // namespace tinygui
551#endif