Skip to content

Commit 660839c

Browse files
committed
Implement monitor auto-positioning
1 parent 29970fd commit 660839c

File tree

5 files changed

+217
-44
lines changed

5 files changed

+217
-44
lines changed

include/scratchcpp/monitor.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,12 @@ class LIBSCRATCHCPP_EXPORT Monitor : public Entity
8585
bool discrete() const;
8686
void setDiscrete(bool discrete);
8787

88-
static Rect getInitialPosition(const std::vector<std::shared_ptr<Monitor>> &other, int monitorWidth, int monitorHeight);
8988
bool needsAutoPosition() const;
89+
void autoPosition(const std::vector<std::shared_ptr<Monitor>> &allMonitors);
9090

9191
private:
92+
static bool monitorRectsIntersect(const Rect &a, const Rect &b);
93+
9294
spimpl::unique_impl_ptr<MonitorPrivate> impl;
9395
};
9496

src/engine/internal/engine.cpp

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1851,11 +1851,6 @@ void Engine::addVarOrListMonitor(std::shared_ptr<Monitor> monitor, Target *targe
18511851
monitor->setValueChangeFunction(changeFunc);
18521852
}
18531853

1854-
// Auto-position the monitor
1855-
Rect rect = Monitor::getInitialPosition(m_monitors, monitor->width(), monitor->height());
1856-
monitor->setX(rect.left());
1857-
monitor->setY(rect.top());
1858-
18591854
m_monitors.push_back(monitor);
18601855
m_monitorAdded(monitor.get());
18611856

src/scratch/monitor.cpp

Lines changed: 117 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -253,21 +253,130 @@ void Monitor::setDiscrete(bool discrete)
253253
impl->discrete = discrete;
254254
}
255255

256-
/*! Returns true if the monitor needs auto positioning. */
256+
/*! Returns true if the monitor needs auto positioning. The renderer should call autoPosition() as soon as it knows the monitor size. */
257257
bool Monitor::needsAutoPosition() const
258258
{
259259
return impl->needsAutoPosition;
260260
}
261261

262-
/*! Returns the initial position of a monitor. */
263-
Rect Monitor::getInitialPosition(const std::vector<std::shared_ptr<Monitor>> &other, int monitorWidth, int monitorHeight)
262+
/*!
263+
* Auto-positions the monitor with the other monitors.
264+
* \note Call this only when the monitor size is known.
265+
*/
266+
void Monitor::autoPosition(const std::vector<std::shared_ptr<Monitor>> &allMonitors)
264267
{
265-
// TODO: Implement this like Scratch has: https://github.com/scratchfoundation/scratch-gui/blob/010e27937ecff531f23bfcf3c711cd6e565cc7f9/src/reducers/monitor-layout.js#L161-L243
266-
// Place the monitor randomly
268+
// https://github.com/scratchfoundation/scratch-gui/blob/010e27937ecff531f23bfcf3c711cd6e565cc7f9/src/reducers/monitor-layout.js#L161-L243
269+
if (!impl->needsAutoPosition)
270+
std::cout << "warning: auto-positioning already positioned monitor (" << impl->name << ")" << std::endl;
271+
272+
impl->needsAutoPosition = false;
273+
274+
// Try all starting positions for the new monitor to find one that doesn't intersect others
275+
std::vector<int> endXs = { 0 };
276+
std::vector<int> endYs = { 0 };
277+
int lastX = 0;
278+
int lastY = 0;
279+
bool haveLastX = false;
280+
bool haveLastY = false;
281+
282+
for (const auto monitor : allMonitors) {
283+
if (monitor.get() != this) {
284+
int x = monitor->x() + monitor->width();
285+
x = std::ceil(x / 50.0) * 50; // Try to choose a sensible "tab width" so more monitors line up
286+
endXs.push_back(x);
287+
endYs.push_back(std::ceil(monitor->y() + monitor->height()));
288+
}
289+
}
290+
291+
std::sort(endXs.begin(), endXs.end());
292+
std::sort(endYs.begin(), endYs.end());
293+
294+
// We'll use plan B if the monitor doesn't fit anywhere (too long or tall)
295+
bool planB = false;
296+
Rect planBRect;
297+
298+
for (const int x : endXs) {
299+
if (haveLastX && x == lastX)
300+
continue;
301+
302+
lastX = x;
303+
haveLastX = true;
304+
305+
for (const int y : endYs) {
306+
if (haveLastY && y == lastY)
307+
continue;
308+
309+
lastY = y;
310+
haveLastY = true;
311+
312+
const Rect monitorRect(x + PADDING, y + PADDING, x + PADDING + impl->width, y + PADDING + impl->height);
313+
314+
// Intersection testing rect that includes padding
315+
const Rect rect(x, y, x + impl->width + 2 * PADDING, y + impl->height + 2 * PADDING);
316+
317+
bool intersected = false;
318+
319+
for (const auto monitor : allMonitors) {
320+
if (monitor.get() != this) {
321+
const Rect currentRect(monitor->x(), monitor->y(), monitor->x() + monitor->width(), monitor->y() + monitor->height());
322+
323+
if (monitorRectsIntersect(currentRect, rect)) {
324+
intersected = true;
325+
break;
326+
}
327+
}
328+
}
329+
330+
if (intersected) {
331+
continue;
332+
}
333+
334+
// If the rect overlaps the ends of the screen
335+
if (rect.right() > SCREEN_WIDTH || rect.bottom() > SCREEN_HEIGHT) {
336+
// If rect is not too close to completely off-screen, set it as plan B
337+
if (!planB && !(rect.left() + SCREEN_EDGE_BUFFER > SCREEN_WIDTH || rect.top() + SCREEN_EDGE_BUFFER > SCREEN_HEIGHT)) {
338+
planBRect = monitorRect;
339+
planB = true;
340+
}
341+
342+
continue;
343+
}
344+
345+
setX(monitorRect.left());
346+
setY(monitorRect.top());
347+
return;
348+
}
349+
}
350+
351+
// If the monitor is too long to fit anywhere, put it in the leftmost spot available
352+
// that intersects the right or bottom edge and isn't too close to the edge.
353+
if (planB) {
354+
setX(planBRect.left());
355+
setY(planBRect.top());
356+
return;
357+
}
358+
359+
// If plan B fails and there's nowhere reasonable to put it, plan C is to place the monitor randomly
267360
if (!MonitorPrivate::rng)
268361
MonitorPrivate::rng = RandomGenerator::instance().get();
269362

270-
const double randX = std::ceil(MonitorPrivate::rng->randintDouble(0, SCREEN_WIDTH / 2.0));
271-
const double randY = std::ceil(MonitorPrivate::rng->randintDouble(0, SCREEN_HEIGHT - SCREEN_EDGE_BUFFER));
272-
return Rect(randX, randY, randX + monitorWidth, randY + monitorHeight);
363+
const int randX = std::ceil(MonitorPrivate::rng->randintDouble(0, SCREEN_WIDTH / 2.0));
364+
const int randY = std::ceil(MonitorPrivate::rng->randintDouble(0, SCREEN_HEIGHT - SCREEN_EDGE_BUFFER));
365+
setX(randX);
366+
setY(randY);
367+
return;
368+
}
369+
370+
bool Monitor::monitorRectsIntersect(const Rect &a, const Rect &b)
371+
{
372+
// https://github.com/scratchfoundation/scratch-gui/blob/010e27937ecff531f23bfcf3c711cd6e565cc7f9/src/reducers/monitor-layout.js#L152-L158
373+
// If one rectangle is on left side of other
374+
if (a.left() >= b.right() || b.left() >= a.right())
375+
return false;
376+
377+
// If one rectangle is above other
378+
if (a.top() >= b.bottom() || b.top() >= a.bottom())
379+
return false;
380+
381+
return true;
273382
}

test/engine/engine_test.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1742,6 +1742,7 @@ TEST(EngineTest, CreateMissingMonitors)
17421742
ASSERT_EQ(monitor->id(), var->id());
17431743
ASSERT_EQ(monitor->opcode(), "data_variable");
17441744
ASSERT_EQ(monitor->mode(), Monitor::Mode::Default);
1745+
ASSERT_TRUE(monitor->needsAutoPosition());
17451746
ASSERT_FALSE(monitor->visible());
17461747
ASSERT_EQ(block->fields().size(), 1);
17471748

@@ -1764,6 +1765,7 @@ TEST(EngineTest, CreateMissingMonitors)
17641765
ASSERT_EQ(monitor->id(), list->id());
17651766
ASSERT_EQ(monitor->opcode(), "data_listcontents");
17661767
ASSERT_EQ(monitor->mode(), Monitor::Mode::Default);
1768+
ASSERT_TRUE(monitor->needsAutoPosition());
17671769
ASSERT_FALSE(monitor->visible());
17681770
ASSERT_EQ(block->fields().size(), 1);
17691771

test/scratch_classes/monitor_test.cpp

Lines changed: 95 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
#include <scratchcpp/rect.h>
55
#include <scratchcpp/virtualmachine.h>
66
#include <scratchcpp/script.h>
7-
#include <scratch/monitor_p.h>
87
#include <monitorhandlermock.h>
98
#include <randomgeneratormock.h>
109

@@ -20,7 +19,6 @@ using ::testing::_;
2019
static const int PADDING = 5;
2120
static const int SCREEN_WIDTH = 400;
2221
static const int SCREEN_HEIGHT = 300;
23-
static const int SCREEN_EDGE_BUFFER = 40;
2422

2523
TEST(MonitorTest, Constructors)
2624
{
@@ -272,36 +270,103 @@ TEST(MonitorTest, Discrete)
272270
ASSERT_FALSE(monitor.discrete());
273271
}
274272

275-
TEST(MonitorTest, GetInitialPosition)
273+
TEST(MonitorTest, AutoPosition_LineUpTopLeft)
276274
{
277-
std::vector<std::shared_ptr<Monitor>> monitors;
275+
// https://github.com/scratchfoundation/scratch-gui/blob/875bee35f178411b9149ab766d17b5fb88ddd749/test/unit/reducers/monitor-layout-reducer.test.js#L216-L238
278276
const int width = 100;
279277
const int height = 200;
280278

281-
auto monitor1 = std::make_shared<Monitor>("", "");
282-
monitor1->setWidth(PADDING);
283-
monitor1->setHeight(height);
284-
monitor1->setX(100);
285-
monitor1->setY(0);
286-
monitors.push_back(monitor1);
287-
288-
auto monitor2 = std::make_shared<Monitor>("", "");
289-
monitor2->setWidth(width);
290-
monitor2->setHeight(height + PADDING - 100);
291-
monitor2->setX(0);
292-
monitor2->setY(100);
293-
monitors.push_back(monitor2);
294-
295-
RandomGeneratorMock rng;
296-
MonitorPrivate::rng = &rng;
297-
298-
EXPECT_CALL(rng, randintDouble(0, SCREEN_WIDTH / 2.0)).WillOnce(Return(SCREEN_WIDTH / 4.5));
299-
EXPECT_CALL(rng, randintDouble(0, SCREEN_HEIGHT - SCREEN_EDGE_BUFFER)).WillOnce(Return(SCREEN_HEIGHT - SCREEN_EDGE_BUFFER * 2.3));
300-
Rect rect = Monitor::getInitialPosition(monitors, width, height);
301-
ASSERT_EQ(rect.left(), std::ceil(SCREEN_WIDTH / 4.5));
302-
ASSERT_EQ(rect.top(), std::ceil(SCREEN_HEIGHT - SCREEN_EDGE_BUFFER * 2.3));
303-
ASSERT_EQ(rect.width(), width);
304-
ASSERT_EQ(rect.height(), height);
305-
306-
MonitorPrivate::rng = nullptr;
279+
auto monitor = std::make_shared<Monitor>("", "");
280+
monitor->setWidth(width);
281+
monitor->setHeight(height);
282+
283+
// Add monitors to right and bottom, but there is a space in the top left
284+
std::vector<std::shared_ptr<Monitor>> monitors = { std::make_shared<Monitor>("", ""), std::make_shared<Monitor>("", ""), monitor };
285+
286+
monitors[0]->setX(width + 2 * PADDING);
287+
monitors[0]->setY(0);
288+
monitors[0]->setWidth(100);
289+
monitors[0]->setHeight(height);
290+
291+
monitors[1]->setX(0);
292+
monitors[1]->setY(height + 2 * PADDING);
293+
monitors[1]->setWidth(width);
294+
monitors[1]->setHeight(100);
295+
296+
// Check that the added monitor appears in the space
297+
monitor->autoPosition(monitors);
298+
ASSERT_EQ(monitor->width(), width);
299+
ASSERT_EQ(monitor->height(), height);
300+
ASSERT_EQ(monitor->x(), PADDING);
301+
ASSERT_EQ(monitor->y(), PADDING);
302+
ASSERT_FALSE(monitor->needsAutoPosition());
303+
}
304+
305+
TEST(MonitorTest, AutoPosition_LineUpLeft)
306+
{
307+
// https://github.com/scratchfoundation/scratch-gui/blob/875bee35f178411b9149ab766d17b5fb88ddd749/test/unit/reducers/monitor-layout-reducer.test.js#L261-L270
308+
auto monitor = std::make_shared<Monitor>("", "");
309+
monitor->setWidth(20);
310+
monitor->setHeight(20);
311+
312+
const int monitor1EndY = 60;
313+
314+
// Add a monitor that takes up the upper left corner
315+
std::vector<std::shared_ptr<Monitor>> monitors = { std::make_shared<Monitor>("", ""), monitor };
316+
monitors[0]->setX(0);
317+
monitors[0]->setY(0);
318+
monitors[0]->setWidth(100);
319+
monitors[0]->setHeight(monitor1EndY);
320+
321+
// Check that added monitor is under it and lines up left
322+
monitor->autoPosition(monitors);
323+
ASSERT_EQ(monitor->width(), 20);
324+
ASSERT_EQ(monitor->height(), 20);
325+
ASSERT_GE(monitor->y(), monitor1EndY + PADDING);
326+
ASSERT_FALSE(monitor->needsAutoPosition());
327+
}
328+
329+
TEST(MonitorTest, LineUpTop)
330+
{
331+
// https://github.com/scratchfoundation/scratch-gui/blob/875bee35f178411b9149ab766d17b5fb88ddd749/test/unit/reducers/monitor-layout-reducer.test.js#L272-L285
332+
auto monitor = std::make_shared<Monitor>("", "");
333+
monitor->setWidth(20);
334+
monitor->setHeight(20);
335+
336+
const int monitor1EndX = 100;
337+
338+
// Add a monitor that takes up the whole left side
339+
std::vector<std::shared_ptr<Monitor>> monitors = { std::make_shared<Monitor>("", ""), monitor };
340+
monitors[0]->setX(0);
341+
monitors[0]->setY(0);
342+
monitors[0]->setWidth(monitor1EndX);
343+
monitors[0]->setHeight(SCREEN_HEIGHT);
344+
345+
// Check that added monitor is to the right of it and lines up top
346+
monitor->autoPosition(monitors);
347+
ASSERT_EQ(monitor->width(), 20);
348+
ASSERT_EQ(monitor->height(), 20);
349+
ASSERT_EQ(monitor->y(), PADDING);
350+
ASSERT_GE(monitor->x(), monitor1EndX + PADDING);
351+
}
352+
353+
TEST(MonitorTest, AutoPosition_NoRoom)
354+
{
355+
// https://github.com/scratchfoundation/scratch-gui/blob/875bee35f178411b9149ab766d17b5fb88ddd749/test/unit/reducers/monitor-layout-reducer.test.js#L287-L302
356+
auto monitor = std::make_shared<Monitor>("", "");
357+
monitor->setWidth(7);
358+
monitor->setHeight(8);
359+
360+
// Add a monitor that takes up the whole screen
361+
std::vector<std::shared_ptr<Monitor>> monitors = { std::make_shared<Monitor>("", ""), monitor };
362+
monitors[0]->setX(0);
363+
monitors[0]->setY(0);
364+
monitors[0]->setWidth(SCREEN_WIDTH);
365+
monitors[0]->setHeight(SCREEN_HEIGHT);
366+
367+
// Check that added monitor exists somewhere (we don't care where)
368+
monitor->autoPosition(monitors);
369+
ASSERT_EQ(monitor->width(), 7);
370+
ASSERT_EQ(monitor->height(), 8);
371+
ASSERT_FALSE(monitor->needsAutoPosition());
307372
}

0 commit comments

Comments
 (0)