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

Return-To-The-Roots / s25client / 22103445825

17 Feb 2026 02:58PM UTC coverage: 50.799% (-0.01%) from 50.809%
22103445825

Pull #1893

github

web-flow
Merge 5da95941f into 6e122731f
Pull Request #1893: Allow dragging ingameWindows with middle mouse button

13 of 34 new or added lines in 4 files covered. (38.24%)

7 existing lines in 3 files now uncovered.

22803 of 44889 relevant lines covered (50.8%)

42144.72 hits per line

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

76.24
/libs/s25main/WindowManager.cpp
1
// Copyright (C) 2005 - 2024 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 "drivers/ScreenResizeEvent.h"
16
#include "drivers/VideoDriverWrapper.h"
17
#include "files.h"
18
#include "helpers/pointerContainerUtils.h"
19
#include "helpers/reverse.h"
20
#include "ingameWindows/IngameWindow.h"
21
#include "ogl/FontStyle.h"
22
#include "ogl/SoundEffectItem.h"
23
#include "ogl/glFont.h"
24
#include "ogl/saveBitmap.h"
25
#include "gameData/const_gui_ids.h"
26
#include "libsiedler2/PixelBufferBGRA.h"
27
#include "s25util/Log.h"
28
#include "s25util/MyTime.h"
29
#include <algorithm>
30
#include <type_traits>
31

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

284
    unsigned time_now = VIDEODRIVER.GetTickCount();
16✔
285

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

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

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

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

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

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

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

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

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

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

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

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

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

378
void WindowManager::Msg_KeyDown(const KeyEvent& ke)
19✔
379
{
380
    if(ke.alt && (ke.kt == KeyType::Return))
19✔
381
    {
382
        // Switch Fullscreen/Windowed
383
        const auto newScreenSize =
384
          !SETTINGS.video.fullscreen ? SETTINGS.video.fullscreenSize : SETTINGS.video.windowedSize; //-V807
×
385
        VIDEODRIVER.ResizeScreen(newScreenSize, !SETTINGS.video.fullscreen);
×
386
        SETTINGS.video.fullscreen = VIDEODRIVER.IsFullscreen();
×
387
    } else if(ke.kt == KeyType::Print)
19✔
388
        TakeScreenshot();
×
389
    else
390
        RelayKeyboardMessage(&Window::Msg_KeyDown, ke);
19✔
391
}
19✔
392

393
/**
394
 *  Handle resize of the window or change of resolution
395
 */
396
void WindowManager::WindowResized()
9✔
397
{
398
    VIDEODRIVER.RenewViewport();
9✔
399
    Msg_ScreenResize(VIDEODRIVER.GetRenderSize());
9✔
400
}
9✔
401

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

411
    ScreenResizeEvent sr(curRenderSize, elMax(Extent(800, 600), newSize));
11✔
412
    curRenderSize = sr.newSize;
11✔
413

414
    // Don't change fullscreen size (only in menu)
415
    if(!SETTINGS.video.fullscreen)
11✔
416
        SETTINGS.video.windowedSize = VIDEODRIVER.GetWindowSize();
11✔
417

418
    // ist unser Desktop gültig?
419
    if(!curDesktop)
11✔
420
        return;
3✔
421

422
    curDesktop->Msg_ScreenResize(sr);
8✔
423

424
    // IngameWindow verschieben falls nötig, so dass sie komplett sichtbar sind
425
    for(const auto& window : windows)
8✔
426
    {
427
        DrawPoint delta = window->GetPos() + DrawPoint(window->GetSize()) - DrawPoint(sr.newSize);
×
428
        if(delta.x > 0 || delta.y > 0)
×
429
            window->SetPos(window->GetPos() - elMax(delta, DrawPoint(0, 0)));
×
430
    }
431
}
432

433
IngameWindow* WindowManager::GetTopMostWindow() const
132✔
434
{
435
    if(windows.empty())
132✔
436
        return nullptr;
44✔
437
    else
438
        return windows.back().get();
88✔
439
}
440

441
void WindowManager::DoClose(IngameWindow* window)
43✔
442
{
443
    const auto it = helpers::findPtr(windows, window);
43✔
444

445
    RTTR_Assert(it != windows.end());
43✔
446

447
    SetToolTip(nullptr, "");
43✔
448

449
    // Store if this was the active window
450
    const bool isActiveWnd = window == GetTopMostWindow();
43✔
451

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

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

479
void WindowManager::CloseNow(IngameWindow* window)
9✔
480
{
481
    if(!window->ShouldBeClosed())
9✔
482
        window->Close();
9✔
483
    DoClose(window);
9✔
484
}
9✔
485

486
/**
487
 *  Actually process the desktop change
488
 */
489
void WindowManager::DoDesktopSwitch()
30✔
490
{
491
    RTTR_Assert(nextdesktop);
30✔
492
    VIDEODRIVER.ClearScreen();
30✔
493

494
    SetToolTip(nullptr, "");
30✔
495

496
    // If we have a current desktop close all windows
497
    if(curDesktop)
30✔
498
        windows.clear();
26✔
499

500
    // Do the switch
501
    curDesktop = std::move(nextdesktop);
30✔
502
    curDesktop->SetActive(true);
30✔
503

504
    for(auto& nextWnd : nextWnds)
31✔
505
        Show(std::move(nextWnd));
1✔
506
    nextWnds.clear();
30✔
507

508
    if(!VIDEODRIVER.IsLeftDown())
30✔
509
        disable_mouse = false;
30✔
510

511
    // Dummy mouse move to init hovering etc
512
    Msg_MouseMove(MouseCoords(VIDEODRIVER.GetMousePos()));
30✔
513
}
30✔
514

515
void WindowManager::CloseMarkedIngameWnds()
81✔
516
{
517
    auto isWndMarkedForClose = [](const auto& wnd) { return wnd->ShouldBeClosed(); };
121✔
518
    auto it = helpers::find_if(windows, isWndMarkedForClose);
81✔
519
    while(it != windows.end())
115✔
520
    {
521
        DoClose(it->get());
34✔
522
        it = helpers::find_if(windows, isWndMarkedForClose);
34✔
523
    }
524
}
81✔
525

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

548
void WindowManager::SetActiveWindow(Window& wnd)
97✔
549
{
550
    if(curDesktop)
97✔
551
        SetActiveWindowImpl(wnd, *curDesktop, windows);
97✔
552
    if(nextdesktop) // NOLINTNEXTLINE(clang-analyzer-cplusplus.Move)
97✔
553
        SetActiveWindowImpl(wnd, *nextdesktop, nextWnds);
×
554
}
97✔
555

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

574
class WindowManager::Tooltip
575
{
576
    static constexpr unsigned BORDER_SIZE = 2;
577
    const ctrlBaseTooltip* showingCtrl;
578
    const glFont* font;
579
    std::vector<std::string> lines;
580
    unsigned width = 0, height = 0;
581
    unsigned short maxWidth;
582

583
public:
584
    Tooltip(const ctrlBaseTooltip* showingCtrl, const std::string& text, unsigned short maxWidth)
×
585
        : showingCtrl(showingCtrl), font(NormalFont), maxWidth(maxWidth)
×
586
    {
587
        setText(text);
×
588
    }
×
589

590
    auto getShowingCtrl() const { return showingCtrl; }
×
591
    auto getWidth() const { return width; }
×
592

593
    void setText(const std::string& text)
×
594
    {
595
        lines = font->GetWrapInfo(text, maxWidth, maxWidth).CreateSingleStrings(text);
×
596
        if(lines.empty())
×
597
            return;
×
598
        width = 0;
×
599
        for(const auto& line : lines)
×
600
            width = std::max(width, font->getWidth(line));
×
601
        width += BORDER_SIZE * 2;
×
602
        height = lines.size() * font->getHeight() + BORDER_SIZE * 2;
×
603
    }
604

605
    void draw(DrawPoint pos) const
×
606
    {
607
        Window::DrawRectangle(Rect(pos, width, height), 0x9F000000);
×
608
        pos += DrawPoint::all(BORDER_SIZE);
×
609
        const auto fontHeight = font->getHeight();
×
610
        for(const auto& line : lines)
×
611
        {
612
            font->Draw(pos, line, FontStyle::TOP, COLOR_YELLOW);
×
613
            pos.y += fontHeight;
×
614
        }
615
    }
×
616
};
617

618
void WindowManager::SetToolTip(const ctrlBaseTooltip* ttw, const std::string& tooltip, bool updateCurrent)
874✔
619
{
620
    // Max width of tooltip
621
    constexpr unsigned short MAX_TOOLTIP_WIDTH = 260;
874✔
622

623
    if(tooltip.empty())
874✔
624
    {
625
        if(curTooltip && (!ttw || curTooltip->getShowingCtrl() == ttw))
873✔
626
            curTooltip.reset();
×
627
    } else if(updateCurrent)
1✔
628
    {
629
        if(curTooltip && curTooltip->getShowingCtrl() == ttw)
1✔
630
            curTooltip->setText(tooltip);
×
631
    } else
632
        curTooltip = std::make_unique<Tooltip>(ttw, tooltip, MAX_TOOLTIP_WIDTH);
×
633
}
874✔
634

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

648
        // links neben der Maus, wenn es über den Rand gehen würde
649
        if(right_edge > curRenderSize.x)
×
650
            ttPos.x = lastMousePos.x - leftSpacing - curTooltip->getWidth();
×
651
        curTooltip->draw(ttPos);
×
652
    }
653
}
81✔
654

655
SnapOffset WindowManager::snapWindow(Window* wnd, const Rect& wndRect) const
8✔
656
{
657
    const auto snapDistance = static_cast<int>(SETTINGS.interface.windowSnapDistance);
8✔
658
    if(snapDistance == 0)
8✔
659
        return SnapOffset::all(0); // No snapping
×
660

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

666
    const auto setOffsetIfLowerDist = [](int a, int b, unsigned& minDist, int& minOffset) {
72✔
667
        const int offset = b - a;
72✔
668
        const unsigned dist = std::abs(offset);
72✔
669
        if(dist < minDist)
72✔
670
        {
671
            minDist = dist;
12✔
672
            minOffset = offset;
12✔
673
        }
674
    };
72✔
675
    const auto setOffsetIfLowerDistX = [&](int x1, int x2) { setOffsetIfLowerDist(x1, x2, minDist.x, minOffset.x); };
40✔
676
    const auto setOffsetIfLowerDistY = [&](int y1, int y2) { setOffsetIfLowerDist(y1, y2, minDist.y, minOffset.y); };
32✔
677

678
    for(const auto& curWnd : windows)
26✔
679
    {
680
        if(curWnd.get() == wnd)
18✔
681
            continue;
8✔
682

683
        const Rect curWndRect = curWnd->GetBoundaryRect();
10✔
684

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

695
        if(wndRect.left <= (curWndRect.right + snapDistance) && wndRect.right >= (curWndRect.left - snapDistance))
10✔
696
        {
697
            setOffsetIfLowerDistY(wndRect.top, curWndRect.top);
8✔
698
            setOffsetIfLowerDistY(wndRect.top, curWndRect.bottom);
8✔
699
            setOffsetIfLowerDistY(wndRect.bottom, curWndRect.top);
8✔
700
            setOffsetIfLowerDistY(wndRect.bottom, curWndRect.bottom);
8✔
701
        }
702
    }
703

704
    return minOffset;
8✔
705
}
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