MXVK Vulkan Framework 0.24.0
C++20 Vulkan rendering framework for practical 2D and 3D application development with SDL3.
Loading...
Searching...
No Matches
mxvk_console.cpp
Go to the documentation of this file.
1/**
2 * @file mxvk_console.cpp
3 * @brief Implementation of mxvk::VK_Console.
4 */
5
7
8#include "mxvk/mxvk.hpp"
10
11#include <algorithm>
12#include <cmath>
13#include <format>
14#include <sstream>
15
16namespace mxvk {
17
18 void VK_Console::attach(VK_Window &window, const std::string &fontPath, const int fontSize) {
19 windowPtr = &window;
20 setFont(fontPath, fontSize);
21 panel_sprite = nullptr;
22 cursor_sprite = nullptr;
23 scroll_track_sprite = nullptr;
24 scroll_thumb_sprite = nullptr;
25 visible = false;
26 fade_active = false;
27 fade_alpha = 0.0f;
28 fade_start_alpha = 0.0f;
29 fade_target_alpha = 0.0f;
30 fade_start_ns = 0;
31 history_index = -1;
32 cursor_pos = 0;
33 input.clear();
34 scroll_offset = 0;
35 follow_tail = true;
36 scrollbar_dragging = false;
37 scrollbar_drag_offset = 0;
38 }
39
40 void VK_Console::setFont(const std::string &fontPath, const int fontSize) {
41 console_font.reset(fontPath, fontSize);
43 refreshVisibleLineCount();
44 }
45
47 commandCallback = std::move(callback);
48 }
49
50 void VK_Console::setPrompt(const std::string &prompt) {
51 if (!prompt.empty()) {
52 promptText = prompt;
53 }
54 }
55
56 void VK_Console::setVisible(const bool value) noexcept {
57 if (value) {
58 visible = true;
59 fade_start_alpha = fade_alpha;
60 fade_target_alpha = 1.0f;
61 fade_start_ns = SDL_GetTicksNS();
62 fade_active = true;
63 if (windowPtr != nullptr) {
64 SDL_StartTextInput(windowPtr->getSDLWindow());
65 }
66 follow_tail = true;
67 scroll_offset = 0;
68 } else {
69 visible = false;
70 fade_start_alpha = fade_alpha;
71 fade_target_alpha = 0.0f;
72 fade_start_ns = SDL_GetTicksNS();
73 fade_active = true;
74 if (windowPtr != nullptr) {
75 SDL_StopTextInput(windowPtr->getSDLWindow());
76 }
77 }
78 }
79
80 void VK_Console::show() noexcept {
81 setVisible(true);
82 }
83
84 void VK_Console::hide() noexcept {
85 setVisible(false);
86 }
87
88 void VK_Console::toggle() noexcept {
89 if (visible) {
90 hide();
91 } else {
92 show();
93 }
94 }
95
96 bool VK_Console::isVisible() const noexcept {
97 return visible || fade_alpha > 0.0f || fade_active;
98 }
99
100 void VK_Console::setMaxLines(const std::size_t maxLines) {
101 max_lines = std::max<std::size_t>(1, maxLines);
102 trimOutputToLimits();
103 invalidateWrappedCache();
104 scroll_offset = std::min(scroll_offset, maxScrollOffset());
105 }
106
107 void VK_Console::setMaxVisibleLines(const std::size_t maxVisibleLines) {
108 max_visible_lines = std::max<std::size_t>(1, maxVisibleLines);
109 }
110
112 invalidateWrappedCache();
113 scroll_offset = std::min(scroll_offset, maxScrollOffset());
114 }
115
116 const std::string &VK_Console::inputBuffer() const noexcept {
117 return input;
118 }
119
120 void VK_Console::printLine(const std::string &line, const SDL_Color color) {
121 std::size_t start = 0;
122 while (start <= line.size()) {
123 const std::size_t nl = line.find('\n', start);
124 if (nl == std::string::npos) {
125 pushOutputLine(line.substr(start), color);
126 break;
127 }
128
129 pushOutputLine(line.substr(start, nl - start), color);
130 start = nl + 1;
131
132 // Preserve a trailing newline as an empty visual line.
133 if (start == line.size()) {
134 pushOutputLine(std::string{}, color);
135 break;
136 }
137 }
138
139 trimOutputToLimits();
140 invalidateWrappedCache();
141 if (follow_tail) {
142 scroll_offset = 0;
143 } else {
144 scroll_offset = std::min(scroll_offset, maxScrollOffset());
145 }
146 }
147
149 total_line_chars = 0;
150 lines.clear();
151 invalidateWrappedCache();
152 input.clear();
153 cursor_pos = 0;
154 scroll_offset = 0;
155 follow_tail = true;
156 history_index = -1;
157 }
158
159 void VK_Console::pushOutputLine(std::string line) {
160 pushOutputLine(std::move(line), text_color);
161 }
162
163 void VK_Console::pushOutputLine(std::string line, const SDL_Color color) {
164 if (max_total_chars > 0 && line.size() > max_total_chars) {
165 line = line.substr(line.size() - max_total_chars);
166 }
167
168 total_line_chars += line.size();
169 lines.push_back(OutputLine{std::move(line), color});
170 wrapped_cache_dirty = true;
171 }
172
173 void VK_Console::trimOutputToLimits() {
174 while (!lines.empty() && (lines.size() > max_lines || total_line_chars > max_total_chars)) {
175 total_line_chars -= lines.front().text.size();
176 lines.pop_front();
177 }
178 wrapped_cache_dirty = true;
179 }
180
181 std::size_t VK_Console::effectiveVisibleLineCount() const noexcept {
182 return std::max<std::size_t>(1, std::min(max_visible_lines, last_visible_line_count));
183 }
184
185 std::size_t VK_Console::maxScrollOffset() const noexcept {
186 const int width = usableTextWidth();
187 ensureWrappedCache(width);
188 const std::size_t total_rows = wrapped_cache_line_count;
189 const std::size_t visible_rows = effectiveVisibleLineCount();
190 if (total_rows <= visible_rows) {
191 return 0;
192 }
193 return total_rows - visible_rows;
194 }
195
196 int VK_Console::measureTextWidth(const std::string &text) const {
197 if (windowPtr == nullptr || text.empty()) {
198 return 0;
199 }
200
201 int width = 0;
202 int height = 0;
203 if (!windowPtr->getTextDimensions(text, width, height, console_font)) {
204 return 0;
205 }
206 return width;
207 }
208
209 int VK_Console::usableTextWidth() const {
210 constexpr int padding = 10;
211 const int reserved_scrollbar = (scrollbar_h > 0) ? (scrollbar_w + padding) : 0;
212 return std::max(1, panel_w - (padding * 2) - reserved_scrollbar);
213 }
214
215 void VK_Console::invalidateWrappedCache() {
216 wrapped_cache_dirty = true;
217 wrapped_cache_width = -1;
218 }
219
220 void VK_Console::ensureWrappedCache(const int maxWidth) const {
221 if (!wrapped_cache_dirty && wrapped_cache_width == maxWidth) {
222 return;
223 }
224
225 wrapped_cache.clear();
226 wrapped_cache_line_count = 0;
227 wrapped_cache_width = maxWidth;
228 wrapped_cache_dirty = false;
229
230 for (const OutputLine &line : lines) {
231 const std::vector<std::string> rows = wrapTextToWidth(line.text, maxWidth);
232 wrapped_cache_line_count += rows.size();
233 for (const std::string &row : rows) {
234 wrapped_cache.push_back(OutputLine{row, line.color});
235 }
236 }
237 }
238
239 std::vector<std::string> VK_Console::wrapTextToWidth(const std::string &text, const int maxWidth) const {
240 std::vector<std::string> wrapped;
241 if (text.empty()) {
242 wrapped.emplace_back();
243 return wrapped;
244 }
245
246 if (maxWidth <= 0) {
247 wrapped.push_back(text);
248 return wrapped;
249 }
250
251 auto appendChunk = [&](std::string chunk) {
252 if (!chunk.empty() || wrapped.empty()) {
253 wrapped.push_back(std::move(chunk));
254 }
255 };
256
257 std::string current;
258 std::size_t index = 0;
259 while (index < text.size()) {
260 const std::size_t tokenStart = index;
261 const bool isSpace = (text[index] == ' ' || text[index] == '\t');
262 while (index < text.size() && ((text[index] == ' ' || text[index] == '\t') == isSpace)) {
263 ++index;
264 }
265
266 const std::string token = text.substr(tokenStart, index - tokenStart);
267 const std::string combined = current + token;
268 if (!current.empty() && measureTextWidth(combined) > maxWidth) {
269 appendChunk(std::move(current));
270 current.clear();
271 }
272
273 if (measureTextWidth(current + token) <= maxWidth) {
274 current += token;
275 continue;
276 }
277
278 if (!current.empty()) {
279 appendChunk(std::move(current));
280 current.clear();
281 }
282
283 std::string fragment;
284 for (char ch : token) {
285 const std::string candidate = fragment + ch;
286 if (!fragment.empty() && measureTextWidth(candidate) > maxWidth) {
287 appendChunk(std::move(fragment));
288 fragment.clear();
289 }
290 fragment.push_back(ch);
291 if (measureTextWidth(fragment) > maxWidth) {
292 const char last = fragment.back();
293 fragment.pop_back();
294 appendChunk(std::move(fragment));
295 fragment.clear();
296 fragment.push_back(last);
297 }
298 }
299 current += fragment;
300 }
301
302 appendChunk(std::move(current));
303 return wrapped;
304 }
305
306 void VK_Console::scrollUp(const std::size_t amount) {
307 if (amount == 0) {
308 return;
309 }
310
311 const std::size_t max_offset = maxScrollOffset();
312 if (max_offset == 0) {
313 return;
314 }
315
316 follow_tail = false;
317 scroll_offset = std::min(max_offset, scroll_offset + amount);
318 }
319
320 void VK_Console::scrollDown(const std::size_t amount) {
321 if (amount == 0) {
322 return;
323 }
324
325 if (scroll_offset <= amount) {
326 scroll_offset = 0;
327 follow_tail = true;
328 return;
329 }
330
331 scroll_offset -= amount;
332 }
333
334 std::vector<std::string> VK_Console::tokenize(const std::string &line) {
335 std::istringstream stream(line);
336 std::vector<std::string> tokens;
337 std::string token;
338 while (stream >> token) {
339 tokens.push_back(token);
340 }
341 return tokens;
342 }
343
344 bool VK_Console::handleDefaultCommand(const std::vector<std::string> &args, std::ostream &output) {
345 if (args.empty()) {
346 return true;
347 }
348
349 const std::string &cmd = args.front();
350 if (cmd == "clear") {
351 clear();
352 return true;
353 }
354 return false;
355 }
356
357 void VK_Console::submitInput() {
358 if (input.empty() || windowPtr == nullptr) {
359 input.clear();
360 cursor_pos = 0;
361 history_index = -1;
362 return;
363 }
364
365 printLine(promptText + input);
366 history.push_back(input);
367 history_index = -1;
368
369 std::ostringstream reply;
370 const std::vector<std::string> args = tokenize(input);
371
372 bool handled = handleDefaultCommand(args, reply);
373 if (!handled && commandCallback) {
374 handled = commandCallback(*windowPtr, args, reply);
375 }
376 if (!handled && !args.empty()) {
377 reply << std::format("Unknown command: {}", args.front());
378 }
379
380 const std::string out = reply.str();
381 if (!out.empty()) {
382 printLine(out);
383 }
384
385 input.clear();
386 cursor_pos = 0;
387 }
388
389 void VK_Console::historyUp() {
390 if (history.empty()) {
391 return;
392 }
393
394 if (history_index < 0) {
395 history_index = static_cast<int>(history.size()) - 1;
396 } else if (history_index > 0) {
397 --history_index;
398 }
399
400 if (history_index >= 0 && history_index < static_cast<int>(history.size())) {
401 input = history[static_cast<std::size_t>(history_index)];
402 cursor_pos = input.size();
403 }
404 }
405
406 void VK_Console::historyDown() {
407 if (history.empty()) {
408 return;
409 }
410
411 if (history_index < 0) {
412 return;
413 }
414
415 if (history_index < static_cast<int>(history.size()) - 1) {
416 ++history_index;
417 input = history[static_cast<std::size_t>(history_index)];
418 cursor_pos = input.size();
419 } else {
420 history_index = -1;
421 input.clear();
422 cursor_pos = 0;
423 }
424 }
425
426 void VK_Console::ensurePanelSprite() {
427 if (panel_sprite != nullptr || windowPtr == nullptr) {
428 return;
429 }
430
431 SDL_Surface *surface = SDL_CreateSurface(2, 2, SDL_PIXELFORMAT_RGBA32);
432 if (surface == nullptr) {
433 return;
434 }
435
436 const SDL_PixelFormatDetails *const fmt = SDL_GetPixelFormatDetails(SDL_PIXELFORMAT_RGBA32);
437 SDL_FillSurfaceRect(surface, nullptr, SDL_MapRGBA(fmt, nullptr, 0, 0, 0, 170));
438 try {
439 panel_sprite = windowPtr->createSprite(surface);
440 } catch (...) {
441 panel_sprite = nullptr;
442 }
443
444 SDL_DestroySurface(surface);
445 }
446
447 void VK_Console::ensureCursorSprite() {
448 if (cursor_sprite != nullptr || windowPtr == nullptr) {
449 return;
450 }
451
452 SDL_Surface *surface = SDL_CreateSurface(2, 2, SDL_PIXELFORMAT_RGBA32);
453 if (surface == nullptr) {
454 return;
455 }
456
457 const SDL_PixelFormatDetails *const fmt = SDL_GetPixelFormatDetails(SDL_PIXELFORMAT_RGBA32);
458 SDL_FillSurfaceRect(surface, nullptr, SDL_MapRGBA(fmt, nullptr, 210, 255, 220, 255));
459 try {
460 cursor_sprite = windowPtr->createSprite(surface);
461 } catch (...) {
462 cursor_sprite = nullptr;
463 }
464
465 SDL_DestroySurface(surface);
466 }
467
468 void VK_Console::ensureScrollSprites() {
469 if (windowPtr == nullptr) {
470 return;
471 }
472
473 if (scroll_track_sprite == nullptr) {
474 SDL_Surface *trackSurface = SDL_CreateSurface(2, 2, SDL_PIXELFORMAT_RGBA32);
475 if (trackSurface != nullptr) {
476 const SDL_PixelFormatDetails *const fmt = SDL_GetPixelFormatDetails(SDL_PIXELFORMAT_RGBA32);
477 SDL_FillSurfaceRect(trackSurface, nullptr, SDL_MapRGBA(fmt, nullptr, 255, 255, 255, 80));
478 try {
479 scroll_track_sprite = windowPtr->createSprite(trackSurface);
480 } catch (...) {
481 scroll_track_sprite = nullptr;
482 }
483 SDL_DestroySurface(trackSurface);
484 }
485 }
486
487 if (scroll_thumb_sprite == nullptr) {
488 SDL_Surface *thumbSurface = SDL_CreateSurface(2, 2, SDL_PIXELFORMAT_RGBA32);
489 if (thumbSurface != nullptr) {
490 const SDL_PixelFormatDetails *const fmt = SDL_GetPixelFormatDetails(SDL_PIXELFORMAT_RGBA32);
491 SDL_FillSurfaceRect(thumbSurface, nullptr, SDL_MapRGBA(fmt, nullptr, 190, 225, 255, 180));
492 try {
493 scroll_thumb_sprite = windowPtr->createSprite(thumbSurface);
494 } catch (...) {
495 scroll_thumb_sprite = nullptr;
496 }
497 SDL_DestroySurface(thumbSurface);
498 }
499 }
500 }
501
502 void VK_Console::updatePanelLayout() {
503 if (windowPtr == nullptr) {
504 return;
505 }
506
507 const VkExtent2D extent = windowPtr->getSwapchainExtent();
508 const int screen_w = static_cast<int>(extent.width);
509 const int screen_h = static_cast<int>(extent.height);
510 if (screen_w <= 0 || screen_h <= 0) {
511 return;
512 }
513
514 constexpr int horizontal_margin = 10;
515 constexpr int top_margin = 8;
516 constexpr int bottom_margin = 24;
517
518 panel_x = horizontal_margin;
519 panel_y = top_margin;
520 panel_w = std::max(300, screen_w - (horizontal_margin * 2));
521 panel_h = std::clamp((screen_h * 42) / 100, 180, std::max(180, screen_h - bottom_margin));
522 }
523
524 void VK_Console::refreshVisibleLineCount() {
525 if (windowPtr == nullptr) {
526 return;
527 }
528
529 updatePanelLayout();
530
531 int glyph_w = 8;
532 int glyph_h = 18;
533 if (!windowPtr->getTextDimensions("M", glyph_w, glyph_h, console_font)) {
534 glyph_h = 18;
535 }
536
537 const int line_height = std::max(1, glyph_h + 2);
538 const int padding = 10;
539 const int title_y = panel_y + padding;
540 const int first_line_y = title_y + line_height;
541 const int input_y = panel_y + panel_h - padding - line_height;
542 const int available_height = std::max(line_height, input_y - first_line_y - 6);
543
544 content_top_y = first_line_y;
545 content_bottom_y = first_line_y + available_height;
546 last_visible_line_count = std::max<std::size_t>(1, static_cast<std::size_t>(available_height / line_height));
547 if (follow_tail) {
548 scroll_offset = 0;
549 }
550
551 const std::size_t max_offset = maxScrollOffset();
552 scroll_offset = std::min(scroll_offset, max_offset);
553 updateScrollbarGeometry();
554 }
555
556 void VK_Console::updateScrollbarGeometry() {
557 const int padding = 10;
558 scrollbar_w = 10;
559 scrollbar_x = panel_x + panel_w - padding - scrollbar_w;
560 scrollbar_y = content_top_y;
561 scrollbar_h = std::max(0, content_bottom_y - content_top_y);
562
563 if (scrollbar_h <= 0) {
564 scrollbar_thumb_h = 0;
565 scrollbar_thumb_y = scrollbar_y;
566 return;
567 }
568
569 ensureWrappedCache(usableTextWidth());
570 const std::size_t total_lines = wrapped_cache_line_count;
571 const std::size_t visible_lines = effectiveVisibleLineCount();
572
573 if (total_lines <= visible_lines) {
574 scrollbar_thumb_h = scrollbar_h;
575 scrollbar_thumb_y = scrollbar_y;
576 return;
577 }
578
579 const std::size_t max_offset = maxScrollOffset();
580 const double visible_ratio = static_cast<double>(visible_lines) / static_cast<double>(total_lines);
581 scrollbar_thumb_h = std::clamp(static_cast<int>(std::lround(static_cast<double>(scrollbar_h) * visible_ratio)), 16, scrollbar_h);
582
583 const int travel = std::max(0, scrollbar_h - scrollbar_thumb_h);
584 if (travel == 0 || max_offset == 0) {
585 scrollbar_thumb_y = scrollbar_y;
586 return;
587 }
588
589 const double ratio_top = static_cast<double>(scroll_offset) / static_cast<double>(max_offset);
590 const int offset_px = static_cast<int>(std::lround((1.0 - ratio_top) * static_cast<double>(travel)));
591 scrollbar_thumb_y = scrollbar_y + std::clamp(offset_px, 0, travel);
592 }
593
594 bool VK_Console::isPointInScrollbar(const int x, const int y) const noexcept {
595 return scrollbar_h > 0 && x >= scrollbar_x && x < (scrollbar_x + scrollbar_w) && y >= scrollbar_y && y < (scrollbar_y + scrollbar_h);
596 }
597
598 bool VK_Console::isPointInScrollbarThumb(const int x, const int y) const noexcept {
599 return scrollbar_thumb_h > 0 && x >= scrollbar_x && x < (scrollbar_x + scrollbar_w) && y >= scrollbar_thumb_y && y < (scrollbar_thumb_y + scrollbar_thumb_h);
600 }
601
602 void VK_Console::updateScrollFromThumbY(const int thumbY) {
603 const std::size_t max_offset = maxScrollOffset();
604 if (max_offset == 0) {
605 scroll_offset = 0;
606 follow_tail = true;
607 return;
608 }
609
610 const int travel = std::max(0, scrollbar_h - scrollbar_thumb_h);
611 if (travel == 0) {
612 scroll_offset = 0;
613 follow_tail = true;
614 return;
615 }
616
617 const int clamped_thumb_y = std::clamp(thumbY, scrollbar_y, scrollbar_y + travel);
618 scrollbar_thumb_y = clamped_thumb_y;
619 const double ratio_bottom_to_top = static_cast<double>(clamped_thumb_y - scrollbar_y) / static_cast<double>(travel);
620 const double ratio_top = 1.0 - ratio_bottom_to_top;
621 scroll_offset = static_cast<std::size_t>(std::lround(ratio_top * static_cast<double>(max_offset)));
622 scroll_offset = std::min(scroll_offset, max_offset);
623 follow_tail = (scroll_offset == 0);
624 }
625
626 void VK_Console::handleEvent(const SDL_Event &event) {
627 if (event.type == SDL_EVENT_KEY_DOWN && (event.key.key == SDLK_F3)) {
628 toggle();
629 return;
630 }
631
632 if (!visible) {
633 return;
634 }
635
636 refreshVisibleLineCount();
637
638 if (event.type == SDL_EVENT_MOUSE_WHEEL) {
639 if (event.wheel.y > 0) {
640 scrollUp(static_cast<std::size_t>(event.wheel.y));
641 } else if (event.wheel.y < 0) {
642 scrollDown(static_cast<std::size_t>(-event.wheel.y));
643 }
644 return;
645 }
646
647 if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN && event.button.button == SDL_BUTTON_LEFT) {
648 const int mx = event.button.x;
649 const int my = event.button.y;
650 if (isPointInScrollbarThumb(mx, my)) {
651 scrollbar_dragging = true;
652 scrollbar_drag_offset = my - scrollbar_thumb_y;
653 return;
654 }
655
656 if (isPointInScrollbar(mx, my)) {
657 const std::size_t page = std::max<std::size_t>(1, effectiveVisibleLineCount() - 1);
658 if (my < scrollbar_thumb_y) {
659 scrollUp(page);
660 } else if (my >= (scrollbar_thumb_y + scrollbar_thumb_h)) {
661 scrollDown(page);
662 }
663 return;
664 }
665 }
666
667 if (event.type == SDL_EVENT_MOUSE_BUTTON_UP && event.button.button == SDL_BUTTON_LEFT) {
668 scrollbar_dragging = false;
669 return;
670 }
671
672 if (event.type == SDL_EVENT_MOUSE_MOTION && scrollbar_dragging) {
673 updateScrollFromThumbY(event.motion.y - scrollbar_drag_offset);
674 return;
675 }
676
677 if (event.type == SDL_EVENT_KEY_DOWN) {
678 switch (event.key.key) {
679 case SDLK_RETURN:
680 case SDLK_KP_ENTER:
681 submitInput();
682 return;
683 case SDLK_BACKSPACE:
684 if (cursor_pos > 0 && cursor_pos <= input.size()) {
685 input.erase(cursor_pos - 1, 1);
686 --cursor_pos;
687 }
688 return;
689 case SDLK_DELETE:
690 if (cursor_pos < input.size()) {
691 input.erase(cursor_pos, 1);
692 }
693 return;
694 case SDLK_LEFT:
695 if (cursor_pos > 0) {
696 --cursor_pos;
697 }
698 return;
699 case SDLK_RIGHT:
700 if (cursor_pos < input.size()) {
701 ++cursor_pos;
702 }
703 return;
704 case SDLK_HOME:
705 cursor_pos = 0;
706 return;
707 case SDLK_END:
708 follow_tail = true;
709 scroll_offset = 0;
710 cursor_pos = input.size();
711 return;
712 case SDLK_UP:
713 historyUp();
714 return;
715 case SDLK_DOWN:
716 historyDown();
717 return;
718 case SDLK_PAGEUP:
719 scrollUp(std::max<std::size_t>(1, effectiveVisibleLineCount() - 1));
720 return;
721 case SDLK_PAGEDOWN:
722 scrollDown(std::max<std::size_t>(1, effectiveVisibleLineCount() - 1));
723 return;
724 case SDLK_ESCAPE:
725 hide();
726 return;
727 default:
728 break;
729 }
730 return;
731 }
732
733 if (event.type == SDL_EVENT_TEXT_INPUT) {
734 if (event.text.text[0] != '\0') {
735 input.insert(cursor_pos, event.text.text);
736 cursor_pos += SDL_strlen(event.text.text);
737 }
738 }
739 }
740
742 if (windowPtr == nullptr) {
743 return;
744 }
745
746 if (fade_active) {
747 const Uint64 now = SDL_GetTicksNS();
748 const double elapsed = static_cast<double>(now - fade_start_ns);
749 const double duration = static_cast<double>(fade_duration_ns);
750 const float t = static_cast<float>(std::clamp(elapsed / std::max(1.0, duration), 0.0, 1.0));
751 fade_alpha = fade_start_alpha + ((fade_target_alpha - fade_start_alpha) * t);
752 if (t >= 1.0f) {
753 fade_alpha = fade_target_alpha;
754 fade_active = false;
755 }
756 }
757
758 if (!(visible || fade_alpha > 0.0f)) {
759 return;
760 }
761
762 const float alpha = std::clamp(fade_alpha, 0.0f, 1.0f);
763 if (alpha <= 0.0f) {
764 return;
765 }
766
767 const auto scaledColor = [alpha](const SDL_Color &src) {
768 SDL_Color out = src;
769 const int scaled = static_cast<int>(std::lround(static_cast<double>(src.a) * alpha));
770 out.a = static_cast<Uint8>(std::clamp(scaled, 0, 255));
771 return out;
772 };
773
774 updatePanelLayout();
775 ensurePanelSprite();
776 ensureCursorSprite();
777 ensureScrollSprites();
778 const auto consoleSpriteY = [this](const int y, const int h) {
779 if (sprite_y_origin_top_left) {
780 return std::max(0, y);
781 }
782
783 const int screen_h = static_cast<int>(windowPtr->getSwapchainExtent().height);
784 return std::max(0, screen_h - y - h);
785 };
786
787 if (panel_sprite != nullptr) {
788 SDL_Surface *surface = SDL_CreateSurface(2, 2, SDL_PIXELFORMAT_RGBA32);
789 if (surface != nullptr) {
790 const SDL_PixelFormatDetails *const fmt = SDL_GetPixelFormatDetails(SDL_PIXELFORMAT_RGBA32);
791 const int panel_alpha = static_cast<int>(std::lround(170.0 * alpha));
792 SDL_FillSurfaceRect(surface, nullptr, SDL_MapRGBA(fmt, nullptr, 0, 0, 0, static_cast<Uint8>(std::clamp(panel_alpha, 0, 255))));
793 panel_sprite->updateTexture(surface);
794 SDL_DestroySurface(surface);
795 }
796
797 panel_sprite->drawSpriteRect(panel_x, consoleSpriteY(panel_y, panel_h), panel_w, panel_h);
798 }
799
800 int glyph_w = 8;
801 int glyph_h = 18;
802 if (!windowPtr->getTextDimensions("M", glyph_w, glyph_h, console_font)) {
803 glyph_h = 18;
804 }
805
806 const int line_height = std::max(1, glyph_h + 2);
807 const int padding = 10;
808 const int text_width = usableTextWidth();
809 ensureWrappedCache(text_width);
810
811 int y = panel_y + padding;
812 windowPtr->printText("MXVK Console (F3 to toggle)", panel_x + padding, y, scaledColor(info_color), console_font);
813 y += line_height;
814
815 const int input_y = panel_y + panel_h - padding - line_height;
816 const int available_height = std::max(line_height, input_y - y - 6);
817 content_top_y = y;
818 content_bottom_y = y + available_height;
819 last_visible_line_count = std::max<std::size_t>(1, static_cast<std::size_t>(available_height / line_height));
820 if (follow_tail) {
821 scroll_offset = 0;
822 }
823
824 const std::size_t max_offset = maxScrollOffset();
825 scroll_offset = std::min(scroll_offset, max_offset);
826 updateScrollbarGeometry();
827
828 const std::size_t visible_lines = std::min(effectiveVisibleLineCount(), wrapped_cache.size());
829 const std::size_t start = (wrapped_cache.size() > visible_lines) ? (wrapped_cache.size() - visible_lines - scroll_offset) : 0;
830 const std::size_t end = std::min(wrapped_cache.size(), start + visible_lines);
831 for (std::size_t i = start; i < end; ++i) {
832 windowPtr->printText(wrapped_cache[i].text, panel_x + padding, y, scaledColor(wrapped_cache[i].color), console_font);
833 y += line_height;
834 }
835
836 if (scroll_track_sprite != nullptr && scrollbar_h > 0) {
837 SDL_Surface *surface = SDL_CreateSurface(2, 2, SDL_PIXELFORMAT_RGBA32);
838 if (surface != nullptr) {
839 const SDL_PixelFormatDetails *const fmt = SDL_GetPixelFormatDetails(SDL_PIXELFORMAT_RGBA32);
840 const int track_alpha = static_cast<int>(std::lround(96.0 * alpha));
841 SDL_FillSurfaceRect(surface, nullptr, SDL_MapRGBA(fmt, nullptr, 255, 255, 255, static_cast<Uint8>(std::clamp(track_alpha, 0, 255))));
842 scroll_track_sprite->updateTexture(surface);
843 SDL_DestroySurface(surface);
844 }
845
846 scroll_track_sprite->drawSpriteRect(scrollbar_x, consoleSpriteY(scrollbar_y, scrollbar_h), scrollbar_w, scrollbar_h);
847 }
848
849 if (scroll_thumb_sprite != nullptr && scrollbar_thumb_h > 0) {
850 SDL_Surface *surface = SDL_CreateSurface(2, 2, SDL_PIXELFORMAT_RGBA32);
851 if (surface != nullptr) {
852 const SDL_PixelFormatDetails *const fmt = SDL_GetPixelFormatDetails(SDL_PIXELFORMAT_RGBA32);
853 const int base_alpha = scrollbar_dragging ? 240 : 190;
854 const int thumb_alpha = static_cast<int>(std::lround(static_cast<double>(base_alpha) * alpha));
855 SDL_FillSurfaceRect(surface, nullptr, SDL_MapRGBA(fmt, nullptr, 190, 225, 255, static_cast<Uint8>(std::clamp(thumb_alpha, 0, 255))));
856 scroll_thumb_sprite->updateTexture(surface);
857 SDL_DestroySurface(surface);
858 }
859
860 scroll_thumb_sprite->drawSpriteRect(scrollbar_x,
861 consoleSpriteY(scrollbar_thumb_y, scrollbar_thumb_h),
862 scrollbar_w,
863 scrollbar_thumb_h);
864 }
865
866 if (scroll_offset > 0) {
867 windowPtr->printText(std::format("^ {} line(s) newer below", scroll_offset),
868 panel_x + padding,
869 input_y - line_height,
870 scaledColor(info_color),
871 console_font);
872 }
873
874 const Uint64 now = SDL_GetTicksNS();
875 if (now - last_cursor_toggle_ns > 500000000ULL) {
876 cursor_visible = !cursor_visible;
877 last_cursor_toggle_ns = now;
878 }
879
880 cursor_pos = std::min(cursor_pos, input.size());
881
882 const std::string line = promptText + input;
883 windowPtr->printText(line, panel_x + padding, input_y, scaledColor(prompt_color), console_font);
884
885 if (cursor_visible && cursor_sprite != nullptr) {
886 int prefix_width = 0;
887 int prefix_height = 0;
888 const std::string prefix = promptText + input.substr(0, cursor_pos);
889 if (!windowPtr->getTextDimensions(prefix, prefix_width, prefix_height, console_font)) {
890 prefix_width = static_cast<int>((promptText.size() + cursor_pos) * static_cast<std::size_t>(glyph_w));
891 }
892
893 // Draw a VS Code-like thin insertion caret at the text boundary.
894 const int cursor_w = 2;
895 const int cursor_x = std::max(panel_x + padding, panel_x + padding + prefix_width - 1);
896 const int cursor_y = input_y + 2;
897 const int cursor_h = std::max(4, line_height - 4);
898
899 SDL_Surface *surface = SDL_CreateSurface(2, 2, SDL_PIXELFORMAT_RGBA32);
900 if (surface != nullptr) {
901 const SDL_PixelFormatDetails *const fmt = SDL_GetPixelFormatDetails(SDL_PIXELFORMAT_RGBA32);
902 const int cursor_alpha = static_cast<int>(std::lround(220.0 * alpha));
903 SDL_FillSurfaceRect(surface, nullptr, SDL_MapRGBA(fmt, nullptr, 210, 255, 220, static_cast<Uint8>(std::clamp(cursor_alpha, 0, 255))));
904 cursor_sprite->updateTexture(surface);
905 SDL_DestroySurface(surface);
906 }
907
908 cursor_sprite->drawSpriteRect(cursor_x, consoleSpriteY(cursor_y, cursor_h), cursor_w, cursor_h);
909 }
910 }
911
912} // namespace mxvk
void setVisible(bool value) noexcept
Show or hide the console.
std::function< bool(VK_Window &window, const std::vector< std::string > &args, std::ostream &output)> CommandCallback
void draw()
Draw the current console overlay.
void invalidateLayoutCache()
Recompute cached line wrapping after font or layout metrics change.
const std::string & inputBuffer() const noexcept
void toggle() noexcept
Toggle console visibility.
void handleEvent(const SDL_Event &event)
Handle keyboard/text events for the console.
bool isVisible() const noexcept
void hide() noexcept
Hide console.
void setFont(const std::string &fontPath, int fontSize)
Change only the console font, leaving the window default font unchanged.
void setPrompt(const std::string &prompt)
Set the input prompt text.
void show() noexcept
Show console.
void clear()
Clear all buffered output lines.
void setMaxVisibleLines(std::size_t maxVisibleLines)
Set max number of lines rendered on screen.
void setCommandCallback(CommandCallback callback)
Register a command callback.
void printLine(const std::string &line, SDL_Color color=SDL_Color{255, 255, 255, 255})
Append one line to console output.
void attach(VK_Window &window, const std::string &fontPath, int fontSize=18)
Attach this console to a window and configure console font rendering.
void setMaxLines(std::size_t maxLines)
Set max number of retained output lines.
Main Vulkan window wrapper for MXVK.
Definition mxvk.hpp:37
In-game command console for MXVK / Vulkan windows.
Vulkan 2-D sprite renderer with optional custom shaders and instancing.
Utilities for loading and saving PNG images.
Definition mxvk.hpp:30