Skip to content

Commit b8e612a

Browse files
authored
Merge pull request #580 from scratchcpp/auto_position_monitors
Implement monitor auto-positioning
2 parents f7b6f13 + 60f8a33 commit b8e612a

File tree

8 files changed

+251
-44
lines changed

8 files changed

+251
-44
lines changed

include/scratchcpp/imonitorhandler.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ class LIBSCRATCHCPP_EXPORT IMonitorHandler
1818
virtual void init(Monitor *monitor) = 0;
1919

2020
virtual void onValueChanged(const VirtualMachine *vm) = 0;
21+
virtual void onXChanged(int x) = 0;
22+
virtual void onYChanged(int y) = 0;
2123
virtual void onVisibleChanged(bool visible) = 0;
2224
};
2325

include/scratchcpp/monitor.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,9 +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);
88+
bool needsAutoPosition() const;
89+
void autoPosition(const std::vector<std::shared_ptr<Monitor>> &allMonitors);
8990

9091
private:
92+
static bool monitorRectsIntersect(const Rect &a, const Rect &b);
93+
9194
spimpl::unique_impl_ptr<MonitorPrivate> impl;
9295
};
9396

src/engine/internal/engine.cpp

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1382,6 +1382,7 @@ Monitor *Engine::createListMonitor(std::shared_ptr<List> list, const std::string
13821382
field->setFieldId(listFieldId);
13831383
monitor->block()->addField(field);
13841384
monitor->block()->setCompileFunction(compileFunction);
1385+
monitor->setMode(Monitor::Mode::List);
13851386

13861387
addVarOrListMonitor(monitor, list->target());
13871388
list->setMonitor(monitor.get());
@@ -1851,11 +1852,6 @@ void Engine::addVarOrListMonitor(std::shared_ptr<Monitor> monitor, Target *targe
18511852
monitor->setValueChangeFunction(changeFunc);
18521853
}
18531854

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-
18591855
m_monitors.push_back(monitor);
18601856
m_monitorAdded(monitor.get());
18611857

src/scratch/monitor.cpp

Lines changed: 130 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,10 @@ int Monitor::x() const
180180
void Monitor::setX(int x)
181181
{
182182
impl->x = x;
183+
impl->needsAutoPosition = false;
184+
185+
if (impl->iface)
186+
impl->iface->onXChanged(x);
183187
}
184188

185189
/*! Returns the monitor's y-coordinate. */
@@ -192,6 +196,10 @@ int Monitor::y() const
192196
void Monitor::setY(int y)
193197
{
194198
impl->y = y;
199+
impl->needsAutoPosition = false;
200+
201+
if (impl->iface)
202+
impl->iface->onYChanged(y);
195203
}
196204

197205
/*! Returns true if the monitor is visible. */
@@ -245,15 +253,130 @@ void Monitor::setDiscrete(bool discrete)
245253
impl->discrete = discrete;
246254
}
247255

248-
/*! Returns the initial position of a monitor. */
249-
Rect Monitor::getInitialPosition(const std::vector<std::shared_ptr<Monitor>> &other, int monitorWidth, int monitorHeight)
256+
/*! Returns true if the monitor needs auto positioning. The renderer should call autoPosition() as soon as it knows the monitor size. */
257+
bool Monitor::needsAutoPosition() const
250258
{
251-
// TODO: Implement this like Scratch has: https://github.com/scratchfoundation/scratch-gui/blob/010e27937ecff531f23bfcf3c711cd6e565cc7f9/src/reducers/monitor-layout.js#L161-L243
252-
// Place the monitor randomly
259+
return impl->needsAutoPosition;
260+
}
261+
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)
267+
{
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
253360
if (!MonitorPrivate::rng)
254361
MonitorPrivate::rng = RandomGenerator::instance().get();
255362

256-
const double randX = std::ceil(MonitorPrivate::rng->randintDouble(0, SCREEN_WIDTH / 2.0));
257-
const double randY = std::ceil(MonitorPrivate::rng->randintDouble(0, SCREEN_HEIGHT - SCREEN_EDGE_BUFFER));
258-
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;
259382
}

src/scratch/monitor_p.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ struct MonitorPrivate
3232
double sliderMin = 0;
3333
double sliderMax = 0;
3434
bool discrete = false;
35+
bool needsAutoPosition = true;
3536
static IRandomGenerator *rng;
3637
};
3738

test/engine/engine_test.cpp

Lines changed: 3 additions & 1 deletion
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

@@ -1763,7 +1764,8 @@ TEST(EngineTest, CreateMissingMonitors)
17631764
auto block = monitor->block();
17641765
ASSERT_EQ(monitor->id(), list->id());
17651766
ASSERT_EQ(monitor->opcode(), "data_listcontents");
1766-
ASSERT_EQ(monitor->mode(), Monitor::Mode::Default);
1767+
ASSERT_EQ(monitor->mode(), Monitor::Mode::List);
1768+
ASSERT_TRUE(monitor->needsAutoPosition());
17671769
ASSERT_FALSE(monitor->visible());
17681770
ASSERT_EQ(block->fields().size(), 1);
17691771

test/mocks/monitorhandlermock.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,7 @@ class MonitorHandlerMock : public IMonitorHandler
1111
MOCK_METHOD(void, init, (Monitor *), (override));
1212

1313
MOCK_METHOD(void, onValueChanged, (const VirtualMachine *), (override));
14+
MOCK_METHOD(void, onXChanged, (int), (override));
15+
MOCK_METHOD(void, onYChanged, (int), (override));
1416
MOCK_METHOD(void, onVisibleChanged, (bool), (override));
1517
};

0 commit comments

Comments
 (0)