• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Return-To-The-Roots / s25client / 22797872430

07 Mar 2026 11:05AM UTC coverage: 50.156% (-0.2%) from 50.327%
22797872430

Pull #1890

github

web-flow
Merge 4597ccbf9 into d2a3730c9
Pull Request #1890: Add support for borderless Windows

155 of 626 new or added lines in 44 files covered. (24.76%)

26 existing lines in 9 files now uncovered.

23041 of 45939 relevant lines covered (50.16%)

44513.07 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.89
/libs/s25main/WindowManager.cpp
1
// Copyright (C) 2005 - 2026 Settlers Freaks (sf-team at siedler25.org)
2
//
3
// SPDX-License-Identifier: GPL-2.0-or-later
4

5
#include "WindowManager.h"
6
#include "CollisionDetection.h"
7
#include "Loader.h"
8
#include "Point.h"
9
#include "RttrConfig.h"
10
#include "Settings.h"
11
#include "Window.h"
12
#include "commonDefines.h"
13
#include "desktops/Desktop.h"
14
#include "driver/MouseCoords.h"
15
#include "driver/VideoInterface.h"
16
#include "drivers/ScreenResizeEvent.h"
17
#include "drivers/VideoDriverWrapper.h"
18
#include "files.h"
19
#include "helpers/pointerContainerUtils.h"
20
#include "helpers/reverse.h"
21
#include "ingameWindows/IngameWindow.h"
22
#include "ogl/FontStyle.h"
23
#include "ogl/SoundEffectItem.h"
24
#include "ogl/glFont.h"
25
#include "ogl/saveBitmap.h"
26
#include "gameData/const_gui_ids.h"
27
#include "libsiedler2/PixelBufferBGRA.h"
28
#include "s25util/Log.h"
29
#include "s25util/MyTime.h"
30
#include <algorithm>
31
#include <type_traits>
32

33
namespace {
34
template<typename T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
35
constexpr std::make_unsigned_t<T> square(T x)
6✔
36
{
37
    return x * x;
6✔
38
}
39
} // namespace
40

41
WindowManager::WindowManager()
4✔
42
    : cursor_(Cursor::Hand), disable_mouse(false), lastMousePos(Position::Invalid()), curRenderSize(0, 0),
4✔
43
      lastLeftClickTime(0), lastLeftClickPos(0, 0)
8✔
44
{}
4✔
45

46
WindowManager::~WindowManager() = default;
8✔
47

48
void WindowManager::CleanUp()
1✔
49
{
50
    windows.clear();
1✔
51
    curDesktop.reset();
1✔
52
    nextdesktop.reset();
1✔
53
}
1✔
54

55
void WindowManager::SetCursor(Cursor cursor)
31✔
56
{
57
    cursor_ = cursor;
31✔
58
}
31✔
59

60
void WindowManager::DrawCursor()
81✔
61
{
62
    auto resId = static_cast<unsigned>(cursor_);
81✔
63
    switch(cursor_)
81✔
64
    {
65
        case Cursor::Hand:
80✔
66
        case Cursor::Remove: resId += VIDEODRIVER.IsLeftDown() ? 1 : 0; break;
80✔
67
        default: break;
1✔
68
    }
69
    if(resId)
81✔
70
        LOADER.GetImageN("resource", resId)->DrawFull(VIDEODRIVER.GetMousePos());
162✔
71
}
81✔
72

73
/**
74
 *  Zeichenfunktion WindowManager-Klasse.
75
 *  Zeichnet Desktop und alle Fenster.
76
 */
77
void WindowManager::Draw()
81✔
78
{
79
    // ist ein neuer Desktop eingetragen? Wenn ja, wechseln
80
    if(nextdesktop)
81✔
81
        DoDesktopSwitch();
30✔
82

83
    if(!curDesktop)
81✔
84
        return;
×
85

86
    curDesktop->Msg_PaintBefore();
81✔
87
    curDesktop->Draw();
81✔
88
    curDesktop->Msg_PaintAfter();
81✔
89

90
    // First close all marked windows
91
    CloseMarkedIngameWnds();
81✔
92
    for(auto& wnd : windows)
147✔
93
    {
94
        // If the window is not minimized, call paintAfter
95
        if(!wnd->IsMinimized())
66✔
96
            wnd->Msg_PaintBefore();
66✔
97
        wnd->Draw();
66✔
98
        // If the window is not minimized, call paintAfter
99
        if(!wnd->IsMinimized())
66✔
100
            wnd->Msg_PaintAfter();
66✔
101
    }
102

103
    DrawToolTip();
81✔
104
    DrawCursor();
81✔
105
}
106

107
bool WindowManager::IsDesktopActive() const
77✔
108
{
109
    if(curDesktop)
77✔
110
        return curDesktop->IsActive();
77✔
111
    return false;
×
112
}
113

114
void WindowManager::RelayKeyboardMessage(KeyboardMsgHandler msg, const KeyEvent& ke)
19✔
115
{
116
    // When there is no desktop, don't check it or any window
117
    if(!curDesktop)
19✔
118
        return;
×
119
    if(curDesktop->IsActive())
19✔
120
    {
121
        // Desktop active -> relay msg to desktop
122
        CALL_MEMBER_FN(*curDesktop, msg)(ke);
2✔
123
        curDesktop->RelayKeyboardMessage(msg, ke);
2✔
124
        return;
2✔
125
    }
126

127
    if(windows.empty())
17✔
128
        return; // No windows -> nothing to do
×
129

130
    // ESC or ALT+W closes the active window
131
    const auto escape = (ke.kt == KeyType::Escape);
17✔
132
    if(escape || (ke.c == 'w' && ke.alt))
17✔
133
    {
134
        // Find one which isn't yet marked for closing so multiple ESC in between draw calls can close multiple windows
135
        // ESC doesn't close pinned windows
136
        const auto itActiveWnd = std::find_if(windows.rbegin(), windows.rend(), [escape](const auto& wnd) {
11✔
137
            return !wnd->ShouldBeClosed() && !(escape && wnd->IsPinned());
12✔
138
        });
22✔
139
        if(itActiveWnd != windows.rend() && (*itActiveWnd)->getCloseBehavior() != CloseBehavior::Custom)
11✔
140
            (*itActiveWnd)->Close();
11✔
141
    } else if(!CALL_MEMBER_FN(*windows.back(), msg)(ke)) // send to active window
6✔
142
    {
143
        // If not handled yet, relay to active window
144
        if(!windows.back()->RelayKeyboardMessage(msg, ke))
×
145
        {
146
            // If message was not handled send to desktop
147
            CALL_MEMBER_FN(*curDesktop, msg)(ke);
×
148
            curDesktop->RelayKeyboardMessage(msg, ke);
×
149
        }
150
    }
151
}
152

153
void WindowManager::RelayMouseMessage(MouseMsgHandler msg, const MouseCoords& mc, Window* window)
104✔
154
{
155
    if(!window)
104✔
156
        window = getActiveWindow();
77✔
157
    if(window)
104✔
158
    {
159
        // If no sub-window/control handled the message, let the window itself handle it
160
        if(!window->RelayMouseMessage(msg, mc))
104✔
161
            CALL_MEMBER_FN(*window, msg)(mc);
103✔
162
    }
163
}
104✔
164

165
/**
166
 *  Öffnet ein IngameWindow und fügt es zur Fensterliste hinzu.
167
 */
168
IngameWindow& WindowManager::DoShow(std::unique_ptr<IngameWindow> window, bool mouse)
60✔
169
{
170
    RTTR_Assert(window);
60✔
171
    RTTR_Assert(!helpers::contains(windows, window));
60✔
172
    // No desktop -> Out
173
    if(!curDesktop)
60✔
174
        throw std::runtime_error("No desktop active for window to be shown on");
×
175

176
    SetToolTip(nullptr, "");
60✔
177

178
    // All windows are inserted before the first modal window (shown behind)
179
    auto itModal = helpers::find_if(windows, [](const auto& curWnd) { return curWnd->IsModal(); });
94✔
180
    // Note that if there is no other modal window it will be put at the back which is what we want
181
    auto& result = **windows.emplace(itModal, std::move(window));
60✔
182

183
    // Make the new window active (special cases handled in the function)
184
    SetActiveWindow(result);
60✔
185

186
    // Maus deaktivieren, bis sie losgelassen wurde (Fix des Switch-Anschließend-Drück-Bugs)
187
    if(!VIDEODRIVER.IsTouch())
60✔
188
        disable_mouse = mouse;
17✔
189
    return result;
60✔
190
}
191

192
IngameWindow* WindowManager::ShowAfterSwitch(std::unique_ptr<IngameWindow> window)
1✔
193
{
194
    RTTR_Assert(window);
1✔
195
    nextWnds.emplace_back(std::move(window));
1✔
196
    return nextWnds.back().get();
1✔
197
}
198

199
/**
200
 *  merkt einen Desktop zum Wechsel vor.
201
 *
202
 *  @param[in] desktop       Pointer zum neuen Desktop, auf dem gewechselt werden soll
203
 *  @param[in] data          Daten für den neuen Desktop
204
 */
205
Desktop* WindowManager::Switch(std::unique_ptr<Desktop> desktop)
30✔
206
{
207
    nextdesktop = std::move(desktop);
30✔
208
    // Disable the mouse till the next desktop is shown to avoid e.g. double-switching
209
    disable_mouse = true;
30✔
210
    return nextdesktop.get();
30✔
211
}
212

213
IngameWindow* WindowManager::FindWindowAtPos(const Position& pos) const
10✔
214
{
215
    // Fenster durchgehen ( von hinten nach vorn, da die vordersten ja zuerst geprüft werden müssen !! )
216
    for(const auto& window : helpers::reverse(windows))
10✔
217
    {
218
        // FensterRect für Kollisionsabfrage
219
        Rect window_rect = window->GetDrawRect();
10✔
220

221
        // trifft die Maus auf ein Fenster?
222
        if(IsPointInRect(pos, window_rect))
10✔
223
        {
224
            return window.get();
10✔
225
        }
226
        // Check also if we are in the locked area of a window (e.g. dropdown extends outside of window)
227
        if(window->IsInLockedRegion(pos))
×
228
            return window.get();
×
229
    }
230
    return nullptr;
×
231
}
232

233
IngameWindow* WindowManager::FindNonModalWindow(unsigned id) const
19✔
234
{
235
    auto itWnd = helpers::find_if(
236
      windows, [id](const auto& wnd) { return !wnd->ShouldBeClosed() && !wnd->IsModal() && wnd->GetID() == id; });
46✔
237
    return itWnd == windows.end() ? nullptr : itWnd->get();
19✔
238
}
239

240
Window* WindowManager::findAndActivateWindow(const Position mousePos)
27✔
241
{
242
    Window* activeWindow = nullptr;
27✔
243
    if(!windows.empty())
27✔
244
    {
245
        if(windows.back()->IsModal())
2✔
246
            activeWindow = windows.back().get();
1✔
247
        else
248
            activeWindow = FindWindowAtPos(mousePos);
1✔
249
    }
250
    if(!activeWindow)
27✔
251
        activeWindow = curDesktop.get();
25✔
252
    if(activeWindow && !activeWindow->IsActive())
27✔
253
        SetActiveWindow(*activeWindow);
×
254
    return activeWindow;
27✔
255
}
256

257
Window* WindowManager::getActiveWindow() const
77✔
258
{
259
    if(IsDesktopActive())
77✔
260
        return curDesktop.get();
74✔
261
    if(!windows.empty())
3✔
262
        return windows.back().get();
3✔
263
    return nullptr;
×
264
}
265

266
void WindowManager::Msg_LeftDown(MouseCoords mc)
19✔
267
{
268
    // play click sound
269
    SoundEffectItem* sound = LOADER.GetSoundN("sound", 112);
38✔
270
    if(sound)
19✔
271
        sound->Play(255, false);
19✔
272

273
    Window* activeWindow = findAndActivateWindow(mc.pos);
19✔
274
    // Ignore mouse message, e.g. right after switching desktop, to avoid unwanted clicks
275
    if(!disable_mouse && activeWindow)
19✔
276
        RelayMouseMessage(&Window::Msg_LeftDown, mc, activeWindow);
19✔
277
}
19✔
278

279
void WindowManager::Msg_LeftUp(MouseCoords mc)
19✔
280
{
281
    // Any desktop active?
282
    if(!curDesktop)
19✔
283
        return;
×
284

285
    unsigned time_now = VIDEODRIVER.GetTickCount();
19✔
286

287
    if(VIDEODRIVER.IsTouch())
19✔
288
    {
289
        if(time_now - lastLeftClickTime < TOUCH_DOUBLE_CLICK_INTERVAL)
5✔
290
        {
291
            // Calculate distance between two points
292
            const Point<int> diff = mc.pos - lastLeftClickPos;
2✔
293
            if(square(diff.x) + square(diff.y) <= square(TOUCH_MAX_DOUBLE_CLICK_DISTANCE))
2✔
294
                mc.dbl_click = true;
1✔
295
        }
296

297
    } else if(time_now - lastLeftClickTime < DOUBLE_CLICK_INTERVAL && mc.pos == lastLeftClickPos)
14✔
298
        mc.dbl_click = true;
1✔
299

300
    if(!mc.dbl_click)
19✔
301
    {
302
        // Save values for next potential dbl click
303
        lastLeftClickPos = mc.pos;
17✔
304
        lastLeftClickTime = time_now;
17✔
305
    }
306

307
    // Don't relay message if mouse is disabled (e.g. right after desktop switch)
308
    if(!disable_mouse)
19✔
309
        RelayMouseMessage(&Window::Msg_LeftUp, mc);
18✔
310
    else if(!nextdesktop)
1✔
311
        disable_mouse = false;
1✔
312
}
313

314
void WindowManager::Msg_RightDown(const MouseCoords& mc)
15✔
315
{
316
    if(!curDesktop)
15✔
317
        return;
×
318

319
    // Right-click closes (most) windows, so handle that first
320
    if(!windows.empty())
15✔
321
    {
322
        IngameWindow* foundWindow = FindWindowAtPos(mc.pos);
9✔
323
        if(windows.back()->IsModal())
9✔
324
        {
325
            // We have a modal window -> Activate it
326
            SetActiveWindow(*windows.back());
2✔
327
            // Ignore actions in all other windows
328
            if(foundWindow != GetTopMostWindow())
2✔
329
                return;
×
330
        }
331
        if(foundWindow)
9✔
332
        {
333
            // Close it if requested (unless pinned)
334
            if(foundWindow->getCloseBehavior() == CloseBehavior::Regular)
9✔
335
            {
336
                if(!foundWindow->IsPinned())
7✔
337
                    foundWindow->Close();
6✔
338
                return;
7✔
339
            }
340
        }
341
    }
342
    RelayMouseMessage(&Window::Msg_RightDown, mc, findAndActivateWindow(mc.pos));
8✔
343
}
344

345
void WindowManager::Msg_RightUp(const MouseCoords& mc)
4✔
346
{
347
    RelayMouseMessage(&Window::Msg_RightUp, mc);
4✔
348
}
4✔
349

350
void WindowManager::Msg_MiddleDown(const MouseCoords& mc)
×
351
{
352
    Window* activeWindow = findAndActivateWindow(mc.pos);
×
353

354
    if(activeWindow)
×
355
        RelayMouseMessage(&Window::Msg_MiddleDown, mc, activeWindow);
×
356
}
×
357

358
void WindowManager::Msg_MiddleUp(const MouseCoords& mc)
×
359
{
360
    RelayMouseMessage(&Window::Msg_MiddleUp, mc);
×
361
}
×
362

363
void WindowManager::Msg_WheelUp(const MouseCoords& mc)
×
364
{
365
    RelayMouseMessage(&Window::Msg_WheelUp, mc, findAndActivateWindow(mc.pos));
×
366
}
×
367

368
void WindowManager::Msg_WheelDown(const MouseCoords& mc)
×
369
{
370
    RelayMouseMessage(&Window::Msg_WheelDown, mc, findAndActivateWindow(mc.pos));
×
371
}
×
372

373
void WindowManager::Msg_MouseMove(const MouseCoords& mc)
55✔
374
{
375
    lastMousePos = mc.pos;
55✔
376
    RelayMouseMessage(&Window::Msg_MouseMove, mc);
55✔
377
}
55✔
378

379
void WindowManager::Msg_KeyDown(const KeyEvent& ke)
19✔
380
{
381
    if(ke.alt && (ke.kt == KeyType::Return))
19✔
382
    {
383
        // Switch Fullscreen/Windowed
384
        const auto newMode =
NEW
385
          (SETTINGS.video.displayMode == DisplayMode::Fullscreen) ? DisplayMode::Windowed : DisplayMode::Fullscreen;
×
386
        const auto newScreenSize =
NEW
387
          (newMode == DisplayMode::Fullscreen) ? SETTINGS.video.fullscreenSize : SETTINGS.video.windowedSize;
×
NEW
388
        VIDEODRIVER.ResizeScreen(newScreenSize, newMode);
×
389

NEW
390
        SETTINGS.video.displayMode = VIDEODRIVER.GetDisplayMode();
×
391
    } else if(ke.kt == KeyType::Print)
19✔
392
        TakeScreenshot();
×
393
    else
394
        RelayKeyboardMessage(&Window::Msg_KeyDown, ke);
19✔
395
}
19✔
396

397
/**
398
 *  Handle resize of the window or change of resolution
399
 */
400
void WindowManager::WindowResized()
9✔
401
{
402
    VIDEODRIVER.RenewViewport();
9✔
403
    Msg_ScreenResize(VIDEODRIVER.GetRenderSize());
9✔
404
}
9✔
405

406
/**
407
 *  React to change of the render size
408
 */
409
void WindowManager::Msg_ScreenResize(const Extent& newSize)
14✔
410
{
411
    // Don't handle it if nothing changed
412
    if(newSize == curRenderSize)
14✔
413
        return;
6✔
414

415
    constexpr Extent minSize(VideoDriverWrapper::MinWindowSize.width, VideoDriverWrapper::MinWindowSize.height);
11✔
416
    ScreenResizeEvent sr(curRenderSize, elMax(minSize, newSize));
11✔
417
    curRenderSize = sr.newSize;
11✔
418

419
    // Don't change fullscreen size (only in menu)
420
    if(SETTINGS.video.displayMode == DisplayMode::Windowed)
11✔
421
        SETTINGS.video.windowedSize = VIDEODRIVER.GetWindowSize();
11✔
422

423
    // Desktop (still) valid?
424
    if(!curDesktop)
11✔
425
        return;
3✔
426

427
    curDesktop->Msg_ScreenResize(sr);
8✔
428

429
    // Move IngameWindows so they are completely visible
430
    for(const auto& window : windows)
8✔
431
    {
432
        DrawPoint delta = window->GetPos() + DrawPoint(window->GetSize()) - DrawPoint(sr.newSize);
×
433
        if(delta.x > 0 || delta.y > 0)
×
434
            window->SetPos(window->GetPos() - elMax(delta, DrawPoint(0, 0)));
×
NEW
435
        window->Msg_ScreenResize(sr);
×
436
    }
437
}
438

439
IngameWindow* WindowManager::GetTopMostWindow() const
133✔
440
{
441
    if(windows.empty())
133✔
442
        return nullptr;
45✔
443
    else
444
        return windows.back().get();
88✔
445
}
446

447
void WindowManager::DoClose(IngameWindow* window)
43✔
448
{
449
    const auto it = helpers::findPtr(windows, window);
43✔
450

451
    RTTR_Assert(it != windows.end());
43✔
452

453
    SetToolTip(nullptr, "");
43✔
454

455
    // Store if this was the active window
456
    const bool isActiveWnd = window == GetTopMostWindow();
43✔
457

458
    // Remove from list and notify parent, hold onto it till parent is notified
459
    const auto tmpHolder = std::move(*it);
86✔
460
    windows.erase(it);
43✔
461
    if(isActiveWnd)
43✔
462
    {
463
        if(windows.empty())
35✔
464
            SetActiveWindow(*curDesktop);
25✔
465
        else
466
            SetActiveWindow(*windows.back());
10✔
467
    }
468
    curDesktop->Msg_WindowClosed(*tmpHolder);
43✔
469
}
43✔
470

471
/**
472
 *  Closes _ALL_ windows with the given ID
473
 *
474
 *  @param[in] id ID of the window to be closed
475
 */
476
void WindowManager::Close(unsigned id)
3✔
477
{
478
    for(auto& wnd : windows)
5✔
479
    {
480
        if(wnd->GetID() == id && !wnd->ShouldBeClosed())
2✔
481
            wnd->Close();
2✔
482
    }
483
}
3✔
484

485
void WindowManager::CloseNow(IngameWindow* window)
9✔
486
{
487
    if(!window->ShouldBeClosed())
9✔
488
        window->Close();
9✔
489
    DoClose(window);
9✔
490
}
9✔
491

492
/**
493
 *  Actually process the desktop change
494
 */
495
void WindowManager::DoDesktopSwitch()
30✔
496
{
497
    RTTR_Assert(nextdesktop);
30✔
498
    VIDEODRIVER.ClearScreen();
30✔
499

500
    SetToolTip(nullptr, "");
30✔
501

502
    // If we have a current desktop close all windows
503
    if(curDesktop)
30✔
504
        windows.clear();
26✔
505

506
    // Do the switch
507
    curDesktop = std::move(nextdesktop);
30✔
508
    curDesktop->SetActive(true);
30✔
509

510
    for(auto& nextWnd : nextWnds)
31✔
511
        Show(std::move(nextWnd));
1✔
512
    nextWnds.clear();
30✔
513

514
    if(!VIDEODRIVER.IsLeftDown())
30✔
515
        disable_mouse = false;
30✔
516

517
    // Dummy mouse move to init hovering etc
518
    Msg_MouseMove(MouseCoords(VIDEODRIVER.GetMousePos()));
30✔
519
}
30✔
520

521
void WindowManager::CloseMarkedIngameWnds()
81✔
522
{
523
    auto isWndMarkedForClose = [](const auto& wnd) { return wnd->ShouldBeClosed(); };
121✔
524
    auto it = helpers::find_if(windows, isWndMarkedForClose);
81✔
525
    while(it != windows.end())
115✔
526
    {
527
        DoClose(it->get());
34✔
528
        it = helpers::find_if(windows, isWndMarkedForClose);
34✔
529
    }
530
}
81✔
531

532
template<class T_Windows>
533
void SetActiveWindowImpl(const Window& wnd, Desktop& desktop, T_Windows& windows)
97✔
534
{
535
    auto itWnd = helpers::find_if(windows, [&wnd](const auto& it) { return it.get() == &wnd; });
209✔
536
    if(itWnd != windows.end())
97✔
537
    {
538
        // If we have a modal window, don't make this active unless it is the top most one
539
        if(&wnd != windows.back().get() && helpers::contains_if(windows, [](const auto& it) { return it->IsModal(); }))
88✔
540
        {
541
            return;
8✔
542
        }
543
        desktop.SetActive(false);
64✔
544
        // Move window to the end (itWnd+1 -> itWnd -> end())
545
        std::rotate(itWnd, std::next(itWnd), windows.end());
64✔
546
    } else if(&wnd == &desktop)
25✔
547
        desktop.SetActive(true);
25✔
548
    else
549
        return;
×
550
    for(const auto& curWnd : windows)
188✔
551
        curWnd->SetActive(&wnd == curWnd.get());
99✔
552
}
553

554
void WindowManager::SetActiveWindow(Window& wnd)
97✔
555
{
556
    if(curDesktop)
97✔
557
        SetActiveWindowImpl(wnd, *curDesktop, windows);
97✔
558
    if(nextdesktop) // NOLINTNEXTLINE(clang-analyzer-cplusplus.Move)
97✔
559
        SetActiveWindowImpl(wnd, *nextdesktop, nextWnds);
×
560
}
97✔
561

562
void WindowManager::TakeScreenshot() const
×
563
{
564
    const auto windowSize = VIDEODRIVER.GetWindowSize();
×
565
    libsiedler2::PixelBufferBGRA buffer(windowSize.width, windowSize.height);
×
566
    glReadPixels(0, 0, windowSize.width, windowSize.height, GL_BGRA, GL_UNSIGNED_BYTE, buffer.getPixelPtr());
×
567
    flipVertical(buffer);
×
568
    const bfs::path outFilepath =
569
      RTTRCONFIG.ExpandPath(s25::folders::screenshots) / (s25util::Time::FormatTime("%Y-%m-%d_%H-%i-%s") + ".bmp");
×
570
    try
571
    {
572
        saveBitmap(buffer, outFilepath);
×
573
        LOG.write(_("Screenshot saved to %1%\n")) % outFilepath;
×
574
    } catch(const std::runtime_error& e)
×
575
    {
576
        LOG.write(_("Error writing screenshot: %1%\n")) % e.what();
×
577
    }
578
}
×
579

580
class WindowManager::Tooltip
581
{
582
    static constexpr unsigned BORDER_SIZE = 2;
583
    const ctrlBaseTooltip* showingCtrl;
584
    const glFont* font;
585
    std::vector<std::string> lines;
586
    unsigned width = 0, height = 0;
587
    unsigned short maxWidth;
588

589
public:
590
    Tooltip(const ctrlBaseTooltip* showingCtrl, const std::string& text, unsigned short maxWidth)
×
591
        : showingCtrl(showingCtrl), font(NormalFont), maxWidth(maxWidth)
×
592
    {
593
        setText(text);
×
594
    }
×
595

596
    auto getShowingCtrl() const { return showingCtrl; }
×
597
    auto getWidth() const { return width; }
×
598

599
    void setText(const std::string& text)
×
600
    {
601
        lines = font->GetWrapInfo(text, maxWidth, maxWidth).CreateSingleStrings(text);
×
602
        if(lines.empty())
×
603
            return;
×
604
        width = 0;
×
605
        for(const auto& line : lines)
×
606
            width = std::max(width, font->getWidth(line));
×
607
        width += BORDER_SIZE * 2;
×
608
        height = lines.size() * font->getHeight() + BORDER_SIZE * 2;
×
609
    }
610

611
    void draw(DrawPoint pos) const
×
612
    {
613
        Window::DrawRectangle(Rect(pos, width, height), 0x9F000000);
×
614
        pos += DrawPoint::all(BORDER_SIZE);
×
615
        const auto fontHeight = font->getHeight();
×
616
        for(const auto& line : lines)
×
617
        {
618
            font->Draw(pos, line, FontStyle::TOP, COLOR_YELLOW);
×
619
            pos.y += fontHeight;
×
620
        }
621
    }
×
622
};
623

624
void WindowManager::SetToolTip(const ctrlBaseTooltip* ttw, const std::string& tooltip, bool updateCurrent)
894✔
625
{
626
    // Max width of tooltip
627
    constexpr unsigned short MAX_TOOLTIP_WIDTH = 260;
894✔
628

629
    if(tooltip.empty())
894✔
630
    {
631
        if(curTooltip && (!ttw || curTooltip->getShowingCtrl() == ttw))
893✔
632
            curTooltip.reset();
×
633
    } else if(updateCurrent)
1✔
634
    {
635
        if(curTooltip && curTooltip->getShowingCtrl() == ttw)
1✔
636
            curTooltip->setText(tooltip);
×
637
    } else
638
        curTooltip = std::make_unique<Tooltip>(ttw, tooltip, MAX_TOOLTIP_WIDTH);
×
639
}
894✔
640

641
void WindowManager::DrawToolTip()
81✔
642
{
643
    // Tooltip zeichnen
644
    if(curTooltip && lastMousePos.isValid())
81✔
645
    {
646
        constexpr unsigned cursorWidth = 32;
×
647
        constexpr unsigned cursorPadding = 5;
×
648
        // Horizontal space between mouse position and tooltip border
649
        constexpr unsigned rightSpacing = cursorWidth + cursorPadding;
×
650
        constexpr unsigned leftSpacing = cursorPadding;
×
651
        DrawPoint ttPos = DrawPoint(lastMousePos.x + rightSpacing, lastMousePos.y);
×
652
        unsigned right_edge = ttPos.x + curTooltip->getWidth();
×
653

654
        // links neben der Maus, wenn es über den Rand gehen würde
655
        if(right_edge > curRenderSize.x)
×
656
            ttPos.x = lastMousePos.x - leftSpacing - curTooltip->getWidth();
×
657
        curTooltip->draw(ttPos);
×
658
    }
659
}
81✔
660

661
SnapOffset WindowManager::snapWindow(Window* wnd, const Rect& wndRect) const
8✔
662
{
663
    const auto snapDistance = static_cast<int>(SETTINGS.interface.windowSnapDistance);
8✔
664
    if(snapDistance == 0)
8✔
665
        return SnapOffset::all(0); // No snapping
×
666

667
    /// Progressive minimum distance to another window edge; initial value limits to at most snapDistance
668
    auto minDist = Extent::all(snapDistance + 1);
8✔
669
    /// (Smallest) offset (signed distance) the window should be moved; initially zero, so no snapping
670
    SnapOffset minOffset = SnapOffset::all(0);
8✔
671

672
    const auto setOffsetIfLowerDist = [](int a, int b, unsigned& minDist, int& minOffset) {
72✔
673
        const int offset = b - a;
72✔
674
        const unsigned dist = std::abs(offset);
72✔
675
        if(dist < minDist)
72✔
676
        {
677
            minDist = dist;
12✔
678
            minOffset = offset;
12✔
679
        }
680
    };
72✔
681
    const auto setOffsetIfLowerDistX = [&](int x1, int x2) { setOffsetIfLowerDist(x1, x2, minDist.x, minOffset.x); };
40✔
682
    const auto setOffsetIfLowerDistY = [&](int y1, int y2) { setOffsetIfLowerDist(y1, y2, minDist.y, minOffset.y); };
32✔
683

684
    for(const auto& curWnd : windows)
26✔
685
    {
686
        if(curWnd.get() == wnd)
18✔
687
            continue;
8✔
688

689
        const Rect curWndRect = curWnd->GetBoundaryRect();
10✔
690

691
        // Calculate smallest offset for each parallel pair of window edges, iff the windows overlap along the
692
        // orthogonal axis (± snap distance)
693
        if(wndRect.top <= (curWndRect.bottom + snapDistance) && wndRect.bottom >= (curWndRect.top - snapDistance))
10✔
694
        {
695
            setOffsetIfLowerDistX(wndRect.left, curWndRect.left);
10✔
696
            setOffsetIfLowerDistX(wndRect.left, curWndRect.right);
10✔
697
            setOffsetIfLowerDistX(wndRect.right, curWndRect.left);
10✔
698
            setOffsetIfLowerDistX(wndRect.right, curWndRect.right);
10✔
699
        }
700

701
        if(wndRect.left <= (curWndRect.right + snapDistance) && wndRect.right >= (curWndRect.left - snapDistance))
10✔
702
        {
703
            setOffsetIfLowerDistY(wndRect.top, curWndRect.top);
8✔
704
            setOffsetIfLowerDistY(wndRect.top, curWndRect.bottom);
8✔
705
            setOffsetIfLowerDistY(wndRect.bottom, curWndRect.top);
8✔
706
            setOffsetIfLowerDistY(wndRect.bottom, curWndRect.bottom);
8✔
707
        }
708
    }
709

710
    return minOffset;
8✔
711
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc