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

Return-To-The-Roots / s25client / 20618416952

31 Dec 2025 11:51AM UTC coverage: 50.569% (+0.06%) from 50.506%
20618416952

Pull #1850

github

web-flow
Merge c015ce8be into 109e7720c
Pull Request #1850: Refactor handling of mouse messages

57 of 103 new or added lines in 4 files covered. (55.34%)

14 existing lines in 4 files now uncovered.

22569 of 44630 relevant lines covered (50.57%)

35759.33 hits per line

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

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

5
#include "IngameWindow.h"
6
#include "CollisionDetection.h"
7
#include "Loader.h"
8
#include "RTTR_Assert.h"
9
#include "Settings.h"
10
#include "WindowManager.h"
11
#include "driver/MouseCoords.h"
12
#include "drivers/VideoDriverWrapper.h"
13
#include "helpers/EnumRange.h"
14
#include "helpers/MultiArray.h"
15
#include "helpers/containerUtils.h"
16
#include "ogl/FontStyle.h"
17
#include "ogl/SoundEffectItem.h"
18
#include "ogl/glArchivItem_Bitmap.h"
19
#include "ogl/glFont.h"
20
#include "gameData/const_gui_ids.h"
21
#include "s25util/error.h"
22
#include <algorithm>
23
#include <utility>
24

25
namespace {
26
constexpr Extent ButtonSize(16, 16);
27
constexpr unsigned TitleMargin = 32;
28
} // namespace
29

30
const DrawPoint IngameWindow::posLastOrCenter(DrawPoint::MaxElementValue, DrawPoint::MaxElementValue);
31
const DrawPoint IngameWindow::posCenter(DrawPoint::MaxElementValue - 1, DrawPoint::MaxElementValue);
32
const DrawPoint IngameWindow::posAtMouse(DrawPoint::MaxElementValue - 1, DrawPoint::MaxElementValue - 1);
33

34
const Extent IngameWindow::borderSize(1, 1);
35
IngameWindow::IngameWindow(unsigned id, const DrawPoint& pos, const Extent& size, std::string title,
83✔
36
                           glArchivItem_Bitmap* background, bool modal, CloseBehavior closeBehavior, Window* parent)
83✔
37
    : Window(parent, id, pos, size), title_(std::move(title)), background(background), lastMousePos(0, 0),
83✔
38
      isModal_(modal), closeme(false), isPinned_(false), isMinimized_(false), isMoving(false),
39
      closeBehavior_(closeBehavior)
83✔
40
{
41
    std::fill(buttonStates_.begin(), buttonStates_.end(), ButtonState::Up);
83✔
42
    contentOffset.x = LOADER.GetImageN("resource", 38)->getWidth();     // left border
166✔
43
    contentOffset.y = LOADER.GetImageN("resource", 42)->getHeight();    // title bar
166✔
44
    contentOffsetEnd.x = LOADER.GetImageN("resource", 39)->getWidth();  // right border
166✔
45
    contentOffsetEnd.y = LOADER.GetImageN("resource", 40)->getHeight(); // bottom bar
166✔
46

47
    const auto it = SETTINGS.windows.persistentSettings.find(GetGUIID());
83✔
48
    windowSettings_ = (it == SETTINGS.windows.persistentSettings.cend() ? nullptr : &it->second);
83✔
49

50
    // For compatibility we treat the given height as the window height, not the content height
51
    // First we have to make sure the size is not to small
52
    Window::Resize(elMax(contentOffset + contentOffsetEnd, GetSize()));
83✔
53
    iwHeight = GetSize().y - contentOffset.y - contentOffsetEnd.y;
83✔
54

55
    // Save to settings that window is open
56
    SaveOpenStatus(true);
83✔
57

58
    if(windowSettings_)
83✔
59
    {
60
        // Restore minimized state
61
        if(windowSettings_->isMinimized)
16✔
62
        {
63
            isMinimized_ = true;
1✔
64
            Extent minimizedSize(GetSize().x, contentOffset.y + contentOffsetEnd.y);
1✔
65
            Window::Resize(minimizedSize);
1✔
66
        }
67
        isPinned_ = windowSettings_->isPinned;     // Restore pinned state
16✔
68
        restorePos_ = windowSettings_->restorePos; // Load restorePos
16✔
69
    }
70

71
    // Load last position or center the window
72
    if(pos == posLastOrCenter)
83✔
73
    {
74
        if(windowSettings_ && windowSettings_->lastPos.isValid())
29✔
75
            SetPos(windowSettings_->lastPos, !restorePos_.isValid());
9✔
76
        else
77
            MoveToCenter();
20✔
78
    } else if(pos == posCenter)
54✔
79
        MoveToCenter();
8✔
80
    else if(pos == posAtMouse)
46✔
81
        MoveNextToMouse();
2✔
82
    else
83
        SetPos(pos); // always call SetPos() to update restorePos
44✔
84
}
83✔
85

86
void IngameWindow::Resize(const Extent& newSize)
35✔
87
{
88
    DrawPoint iSize(newSize);
35✔
89
    iSize = elMax(DrawPoint(0, 0), iSize - DrawPoint(contentOffset + contentOffsetEnd));
35✔
90
    SetIwSize(Extent(iSize));
35✔
91
}
35✔
92

93
void IngameWindow::SetIwSize(const Extent& newSize)
47✔
94
{
95
    // Is the window connecting with the bottom screen edge?
96
    const auto atBottom = (GetPos().y + GetSize().y) >= VIDEODRIVER.GetRenderSize().y;
47✔
97

98
    iwHeight = newSize.y;
47✔
99
    Extent wndSize = newSize;
47✔
100
    if(isMinimized_)
47✔
101
        wndSize.y = 0;
11✔
102
    wndSize += contentOffset + contentOffsetEnd;
47✔
103
    Window::Resize(wndSize);
47✔
104

105
    // Adjust restorePos if the window was connecting with the bottom screen edge before being minimized
106
    const auto pos = (atBottom && isMinimized_) ? DrawPoint(restorePos_.x, DrawPoint::MaxElementValue) : restorePos_;
47✔
107

108
    // Reset the position
109
    // 1) to check if parts of the window are out of the visible area
110
    // 2) to re-connect the window with the bottom screen edge, if needed
111
    SetPos(pos, false);
47✔
112
}
47✔
113

114
Extent IngameWindow::GetIwSize() const
22✔
115
{
116
    return Extent(GetSize().x - contentOffset.x - contentOffsetEnd.x, iwHeight);
22✔
117
}
118

119
Extent IngameWindow::GetFullSize() const
×
120
{
121
    return Extent(GetSize().x, contentOffset.y + contentOffsetEnd.y + iwHeight);
×
122
}
123

124
DrawPoint IngameWindow::GetRightBottomBoundary()
86✔
125
{
126
    return DrawPoint(GetSize() - contentOffsetEnd);
86✔
127
}
128

129
void IngameWindow::SetPos(DrawPoint newPos, bool saveRestorePos)
144✔
130
{
131
    const Extent screenSize = VIDEODRIVER.GetRenderSize();
144✔
132
    DrawPoint newRestorePos = newPos;
144✔
133
    // Too far left or right?
134
    if(newPos.x < 0)
144✔
135
        newRestorePos.x = newPos.x = 0;
×
136
    else if(newPos.x + GetSize().x >= screenSize.x)
144✔
137
    {
138
        newPos.x = screenSize.x - GetSize().x;
8✔
139
        newRestorePos.x = DrawPoint::MaxElementValue; // make window stick to the right
8✔
140
    }
141

142
    // Too high or low?
143
    if(newPos.y < 0)
144✔
144
        newRestorePos.y = newPos.y = 0;
×
145
    else if(newPos.y + GetSize().y >= screenSize.y)
144✔
146
    {
147
        newPos.y = screenSize.y - GetSize().y;
10✔
148
        newRestorePos.y = DrawPoint::MaxElementValue; // make window stick to the bottom
10✔
149
    }
150

151
    if(saveRestorePos)
144✔
152
        restorePos_ = newRestorePos;
88✔
153

154
    // if possible save the positions to settings
155
    if(windowSettings_)
144✔
156
    {
157
        windowSettings_->lastPos = newPos;
34✔
158
        if(saveRestorePos)
34✔
159
            windowSettings_->restorePos = newRestorePos;
9✔
160
    }
161

162
    Window::SetPos(newPos);
144✔
163
}
144✔
164

165
void IngameWindow::Close()
53✔
166
{
167
    SaveOpenStatus(false);
53✔
168
    closeme = true;
53✔
169
}
53✔
170

171
void IngameWindow::SetMinimized(bool minimized)
20✔
172
{
173
    Extent fullSize = GetSize();
20✔
174
    if(isMinimized_)
20✔
175
        fullSize.y = iwHeight + contentOffset.y + contentOffsetEnd.y;
9✔
176
    this->isMinimized_ = minimized;
20✔
177
    Resize(fullSize);
20✔
178

179
    // if possible save the minimized state to settings
180
    if(windowSettings_)
20✔
181
        windowSettings_->isMinimized = isMinimized_;
12✔
182
}
20✔
183

184
void IngameWindow::SetPinned(bool pinned)
8✔
185
{
186
    isPinned_ = pinned;
8✔
187

188
    // if possible save the pinned state to settings
189
    if(windowSettings_)
8✔
190
        windowSettings_->isPinned = isPinned_;
3✔
191
}
8✔
192

193
bool IngameWindow::Msg_LeftDown(const MouseCoords& mc)
10✔
194
{
195
    for(const auto btn : helpers::enumRange<IwButton>())
84✔
196
    {
197
        if(IsPointInRect(mc.pos, GetButtonBounds(btn)))
26✔
198
        {
199
            buttonStates_[btn] = ButtonState::Pressed;
9✔
200
            if(btn == IwButton::Title)
9✔
201
            {
202
                // start moving
203
                isMoving = true;
4✔
204
                snapOffset_ = SnapOffset::all(0);
4✔
205
                lastMousePos = mc.pos;
4✔
206
                return true;
4✔
207
            }
208
        }
209
    }
210
    return false;
6✔
211
}
212

213
bool IngameWindow::Msg_LeftUp(const MouseCoords& mc)
8✔
214
{
215
    isMoving = false;
8✔
216

217
    for(const auto btn : helpers::enumRange<IwButton>())
60✔
218
    {
219
        const auto clicked = buttonStates_[btn] == ButtonState::Pressed;
19✔
220
        buttonStates_[btn] = ButtonState::Up;
19✔
221
        if(!clicked)
19✔
222
            continue;
13✔
223

224
        if((btn == IwButton::Close && closeBehavior_ == CloseBehavior::Custom) // no close button
6✔
225
           || (isModal_ // modal windows cannot be pinned or minimized
6✔
226
               && (btn == IwButton::Title || btn == IwButton::PinOrMinimize)))
×
227
            continue;
×
228

229
        if(IsPointInRect(mc.pos, GetButtonBounds(btn)))
6✔
230
        {
231
            switch(btn)
6✔
232
            {
233
                case IwButton::Close: Close(); return true;
5✔
234
                case IwButton::Title:
2✔
235
                    if(SETTINGS.interface.enableWindowPinning && mc.dbl_click)
2✔
236
                    {
237
                        SetMinimized(!IsMinimized());
1✔
238
                        LOADER.GetSoundN("sound", 113)->Play(255, false);
2✔
239
                        return true;
1✔
240
                    }
241
                    break;
1✔
242
                case IwButton::PinOrMinimize:
2✔
243
                    if(SETTINGS.interface.enableWindowPinning)
2✔
244
                    {
245
                        SetPinned(!IsPinned());
1✔
246
                        LOADER.GetSoundN("sound", 111)->Play(255, false);
2✔
247
                    } else
248
                    {
249
                        SetMinimized(!IsMinimized());
1✔
250
                        LOADER.GetSoundN("sound", 113)->Play(255, false);
2✔
251
                    }
252
                    return true;
2✔
253
            }
254
        }
255
    }
256
    return false;
3✔
257
}
258

259
bool IngameWindow::Msg_MouseMove(const MouseCoords& mc)
10✔
260
{
261
    if(isMoving)
10✔
262
    {
263
        // Calculate new window boundary rectangle without snapping
264
        DrawPoint delta = mc.pos - lastMousePos;
5✔
265
        Rect wndRect = GetBoundaryRect();
5✔
266
        RTTR_Assert(wndRect.getOrigin() == GetPos()); // The rest of the code assumes this to be true
5✔
267
        wndRect.move(delta - snapOffset_);
5✔
268

269
        // Try to snap this window to any other
270
        snapOffset_ = WINDOWMANAGER.snapWindow(this, wndRect);
5✔
271

272
        // The cursor position is always relative to the position of the "unsnapped" window; bound the "unsnapped"
273
        // window position to the screen…
274
        DrawPoint newPos = wndRect.getOrigin();
5✔
275
        DrawPoint newPosBounded =
276
          elMin(elMax(newPos, DrawPoint::all(0)), DrawPoint(VIDEODRIVER.GetRenderSize() - wndRect.getSize()));
5✔
277
        // …and use it to fix the mouse position if moved too far
278
        if(newPosBounded != newPos)
5✔
279
            VIDEODRIVER.SetMousePos(newPosBounded - wndRect.getOrigin() + mc.pos);
1✔
280

281
        // Set new position and re-calculate snap offset (window position may have been out of bounds)
282
        SetPos(newPos + snapOffset_);
5✔
283
        snapOffset_ = GetPos() - newPosBounded;
5✔
284

285
        lastMousePos = mc.pos;
5✔
286
        return true;
5✔
287
    } else
288
    {
289
        // Check the buttons
290
        for(const auto btn : helpers::enumRange<IwButton>())
50✔
291
        {
292
            if(IsPointInRect(mc.pos, GetButtonBounds(btn)))
15✔
293
            {
294
                if(!mc.ldown)
2✔
NEW
295
                    buttonStates_[btn] = ButtonState::Hover;
×
296
            } else
297
                buttonStates_[btn] = ButtonState::Up;
13✔
298
        }
299
        return false;
5✔
300
    }
301
}
302

303
void IngameWindow::Draw_()
66✔
304
{
305
    if(isModal_ && !IsActive())
66✔
306
        SetActive(true);
7✔
307
    if(!isMinimized_)
66✔
308
        DrawBackground();
66✔
309
    // Black border
310
    // TODO: It would be better if this was included in the windows size. But the controls are added with absolute
311
    // positions so adding the border to the size would move the border imgs inward into the content.
312
    //
313
    // Solution 1: Use contentOffset for adding controls
314
    //
315
    // Solution 2: Define that all controls added to an ingame window have positions relative to the contentOffset.
316
    //  This needs a change in GetDrawPos to add this offset and also change all control-add-calls but would be much
317
    //  cleaner (no more hard coded offsets and we could restyle the ingame windows easily)
318
    //
319
    const Rect drawRect = GetDrawRect();
66✔
320
    const Rect fullWndRect(drawRect.getOrigin() - borderSize, drawRect.getSize() + borderSize * 2u);
66✔
321
    // Top
322
    DrawRectangle(Rect(fullWndRect.getOrigin(), fullWndRect.getSize().x, borderSize.y), COLOR_BLACK);
66✔
323
    // Left
324
    DrawRectangle(Rect(fullWndRect.getOrigin(), borderSize.x, fullWndRect.getSize().y), COLOR_BLACK);
66✔
325
    // Right
326
    DrawRectangle(Rect(fullWndRect.right - borderSize.x, fullWndRect.top, borderSize.x, fullWndRect.getSize().y),
66✔
327
                  COLOR_BLACK);
328
    // Bottom
329
    DrawRectangle(Rect(fullWndRect.left, fullWndRect.bottom - borderSize.y, fullWndRect.getSize().x, borderSize.y),
66✔
330
                  COLOR_BLACK);
331

332
    // Upper parts
333
    glArchivItem_Bitmap* leftUpperImg = LOADER.GetImageN("resource", 36);
132✔
334
    leftUpperImg->DrawFull(GetPos());
66✔
335
    glArchivItem_Bitmap* rightUpperImg = LOADER.GetImageN("resource", 37);
132✔
336
    rightUpperImg->DrawFull(GetPos() + DrawPoint(GetSize().x - rightUpperImg->getWidth(), 0));
66✔
337

338
    // The buttons
339
    using ButtonStateResIds = helpers::EnumArray<unsigned, ButtonState>;
340
    constexpr ButtonStateResIds closeResIds = {47, 55, 51};
66✔
341
    constexpr ButtonStateResIds minimizeResIds = {48, 56, 52};
66✔
342
    constexpr ButtonStateResIds pinBaseResIds = {47, 47, 51};
66✔
343
    constexpr ButtonStateResIds pinOverlayResIds = {15, 16, 17};
66✔
344
    constexpr ButtonStateResIds unpinOverlayResIds = {18, 19, 20};
66✔
345
    if(closeBehavior_ != CloseBehavior::Custom)
66✔
346
        LOADER.GetImageN("resource", closeResIds[buttonStates_[IwButton::Close]])
60✔
347
          ->DrawFull(GetButtonBounds(IwButton::Close));
120✔
348
    if(!IsModal())
66✔
349
    {
350
        const auto buttonState = buttonStates_[IwButton::PinOrMinimize];
38✔
351
        const auto bounds = GetButtonBounds(IwButton::PinOrMinimize);
38✔
352
        if(SETTINGS.interface.enableWindowPinning)
38✔
353
        {
354
            LOADER.GetImageN("resource", pinBaseResIds[buttonState])->DrawFull(bounds);
74✔
355
            if(isPinned_)
37✔
356
                LOADER.GetImageN("io_new", unpinOverlayResIds[buttonState])->DrawFull(bounds);
4✔
357
            else
358
                LOADER.GetImageN("io_new", pinOverlayResIds[buttonState])->DrawFull(bounds);
70✔
359
        } else
360
            LOADER.GetImageN("resource", minimizeResIds[buttonState])->DrawFull(bounds);
2✔
361
    }
362

363
    // The title bar
364
    unsigned titleIndex;
365
    if(IsActive())
66✔
366
        titleIndex = isMoving ? 44 : 43;
49✔
367
    else
368
        titleIndex = 42;
17✔
369

370
    glArchivItem_Bitmap& titleImg = *LOADER.GetImageN("resource", titleIndex);
132✔
371
    DrawPoint titleImgPos = GetPos() + DrawPoint(leftUpperImg->getWidth(), 0);
66✔
372
    const unsigned titleWidth = GetSize().x - leftUpperImg->getWidth() - rightUpperImg->getWidth();
66✔
373
    // How often should the image be drawn to get the full width
374
    unsigned tileCount = titleWidth / titleImg.getWidth();
66✔
375
    for(unsigned i = 0; i < tileCount; ++i)
6,954✔
376
    {
377
        titleImg.DrawFull(titleImgPos);
6,888✔
378
        titleImgPos.x += titleImg.getWidth();
6,888✔
379
    }
380

381
    // The remaining part (if any)
382
    unsigned remainingTileSize = titleWidth % titleImg.getWidth();
66✔
383
    if(remainingTileSize)
66✔
384
        titleImg.DrawPart(Rect(titleImgPos, remainingTileSize, titleImg.getHeight()));
×
385

386
    // Text on the title bar
387
    NormalFont->Draw(GetPos() + DrawPoint(GetSize().x, titleImg.getHeight()) / 2, title_,
66✔
388
                     FontStyle::CENTER | FontStyle::VCENTER, COLOR_YELLOW);
389

390
    glArchivItem_Bitmap* bottomBorderSideImg = LOADER.GetImageN("resource", 45);
132✔
391
    glArchivItem_Bitmap* bottomBarImg = LOADER.GetImageN("resource", 40);
132✔
392

393
    // Side bars
394
    if(!isMinimized_)
66✔
395
    {
396
        unsigned sideHeight = GetSize().y - leftUpperImg->getHeight() - bottomBorderSideImg->getHeight();
66✔
397

398
        glArchivItem_Bitmap* leftSideImg = LOADER.GetImageN("resource", 38);
132✔
399
        glArchivItem_Bitmap* rightSideImg = LOADER.GetImageN("resource", 39);
132✔
400
        tileCount = sideHeight / leftSideImg->getHeight();
66✔
401
        DrawPoint leftImgPos = GetPos() + DrawPoint(0, leftUpperImg->getHeight());
66✔
402
        DrawPoint rightImgPos = leftImgPos + DrawPoint(GetSize().x - leftSideImg->getWidth(), 0);
66✔
403
        for(unsigned i = 0; i < tileCount; ++i)
6,728✔
404
        {
405
            leftSideImg->DrawFull(leftImgPos);
6,662✔
406
            rightSideImg->DrawFull(rightImgPos);
6,662✔
407
            rightImgPos.y = leftImgPos.y += leftSideImg->getHeight();
6,662✔
408
        }
409

410
        // And the partial part
411
        remainingTileSize = sideHeight % leftSideImg->getHeight();
66✔
412
        if(remainingTileSize)
66✔
413
        {
414
            leftSideImg->DrawPart(Rect(leftImgPos, leftSideImg->getWidth(), remainingTileSize));
×
415
            rightSideImg->DrawPart(Rect(rightImgPos, rightSideImg->getWidth(), remainingTileSize));
×
416
        }
417
    }
418

419
    // Lower bar
420
    const unsigned bottomBarWidth = GetSize().x - bottomBorderSideImg->getWidth() * 2;
66✔
421
    tileCount = bottomBarWidth / bottomBarImg->getWidth();
66✔
422
    DrawPoint bottomImgPos = GetPos() + DrawPoint(bottomBorderSideImg->getWidth(), GetRightBottomBoundary().y);
66✔
423
    for(unsigned i = 0; i < tileCount; ++i)
6,954✔
424
    {
425
        bottomBarImg->DrawFull(bottomImgPos);
6,888✔
426
        bottomImgPos.x += bottomBarImg->getWidth();
6,888✔
427
    }
428

429
    remainingTileSize = bottomBarWidth % bottomBarImg->getWidth();
66✔
430
    if(remainingTileSize)
66✔
431
        bottomBarImg->DrawPart(Rect(bottomImgPos, remainingTileSize, bottomBarImg->getHeight()));
×
432

433
    // Client area
434
    if(!isMinimized_)
66✔
435
    {
436
        Window::Draw_();
66✔
437
        DrawContent();
66✔
438
    }
439

440
    // The 2 rects on the bottom left and right
441
    bottomBorderSideImg->DrawFull(GetPos() + DrawPoint(0, GetSize().y - bottomBorderSideImg->getHeight()));
66✔
442
    bottomBorderSideImg->DrawFull(GetPos() + GetSize() - bottomBorderSideImg->GetSize());
66✔
443
}
66✔
444

445
void IngameWindow::DrawBackground()
66✔
446
{
447
    if(background)
66✔
448
        background->DrawPart(Rect(GetPos() + DrawPoint(contentOffset), GetIwSize()));
2✔
449
}
66✔
450

451
void IngameWindow::MoveToCenter()
28✔
452
{
453
    SetPos(DrawPoint(VIDEODRIVER.GetRenderSize() - GetSize()) / 2);
28✔
454
}
28✔
455

456
void IngameWindow::MoveNextToMouse()
3✔
457
{
458
    // Center vertically and move slightly right
459
    DrawPoint newPos = VIDEODRIVER.GetMousePos() - DrawPoint(-20, GetSize().y / 2);
3✔
460
    SetPos(newPos);
3✔
461
}
3✔
462

463
bool IngameWindow::IsMessageRelayAllowed() const
5✔
464
{
465
    return !isMinimized_ && !isMoving;
5✔
466
}
467

468
void IngameWindow::SaveOpenStatus(bool isOpen) const
136✔
469
{
470
    if(windowSettings_)
136✔
471
        windowSettings_->isOpen = isOpen;
23✔
472
}
136✔
473

474
Rect IngameWindow::GetButtonBounds(IwButton btn) const
145✔
475
{
476
    auto pos = GetPos();
145✔
477
    auto size = ButtonSize;
145✔
478
    switch(btn)
145✔
479
    {
480
        case IwButton::Close: break;
77✔
481
        case IwButton::Title:
17✔
482
            pos.x += TitleMargin;
17✔
483
            size.x = GetSize().x - TitleMargin * 2;
17✔
484
            break;
17✔
485
        case IwButton::PinOrMinimize: pos.x += GetSize().x - ButtonSize.x; break;
51✔
486
    }
487
    return Rect(pos, size);
145✔
488
}
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