Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 44 additions & 3 deletions src/ui/drag_drop_controller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,31 @@ namespace {
return safe;
}

// One axis of a scaled preview. The render transform scales about
// transformOrigin, so the painted box starts before the node position
// whenever the origin is not the leading edge.
struct PreviewAxis {
float position;
float size;
float scale;
float transformOrigin;
float overlayExtent;
};

float clampPreviewPosition(const PreviewAxis& axis) {
const float paintedStart = axis.transformOrigin * (1.0F - axis.scale);
const float paintedEnd = paintedStart + axis.size * axis.scale;
const float minimum = -paintedStart;
const float maximum = axis.overlayExtent - paintedEnd;
if (minimum > maximum) {
// The preview is larger than the overlay, so it cannot fit inside it.
// Keep it covering the overlay instead; it still follows the pointer
// until one of the overlay edges would be uncovered.
return std::clamp(axis.position, maximum, minimum);
}
return std::clamp(axis.position, minimum, maximum);
}

} // namespace

DragDropController::~DragDropController() {
Expand Down Expand Up @@ -336,9 +361,25 @@ void DragDropController::updatePreview(float sceneX, float sceneY) {
}
float localX = 0.0F;
float localY = 0.0F;
if (Node::mapFromScene(m_overlayRoot, sceneX - m_pointerOffsetX, sceneY - m_pointerOffsetY, localX, localY)) {
m_preview->setPosition(localX, localY);
}
// Pointer capture can deliver positions outside the overlay. mapFromScene still
// provides the transformed coordinates when its containment result is false.
(void)Node::mapFromScene(m_overlayRoot, sceneX - m_pointerOffsetX, sceneY - m_pointerOffsetY, localX, localY);
m_preview->setPosition(
clampPreviewPosition({
.position = localX,
.size = m_preview->width(),
.scale = m_preview->scaleX(),
.transformOrigin = m_preview->transformOriginX(),
.overlayExtent = m_overlayRoot->width(),
}),
clampPreviewPosition({
.position = localY,
.size = m_preview->height(),
.scale = m_preview->scaleY(),
.transformOrigin = m_preview->transformOriginY(),
.overlayExtent = m_overlayRoot->height(),
})
);
}

void DragDropController::clearPreview() {
Expand Down
66 changes: 66 additions & 0 deletions tests/ui_tree_reconciler_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1704,6 +1704,72 @@ int main() {
)
&& ok;

if (proxy != nullptr) {
const auto previewBounds = [proxy]() {
float left = 0.0f;
float top = 0.0f;
float right = 0.0f;
float bottom = 0.0f;
Node::transformedBounds(proxy, left, top, right, bottom);
return LayoutRect{.x = left, .y = top, .width = right - left, .height = bottom - top};
};

source->inputArea()->dispatchMotion(-100.0f, -100.0f);
auto bounds = previewBounds();
ok = expect(bounds.x >= -0.001f && bounds.y >= -0.001f, "drag preview stays inside the top-left edge") && ok;

const float leftEdgeX = proxy->x();
const float topEdgeY = proxy->y();
source->inputArea()->dispatchMotion(-100.0f, 80.0f);
ok = expect(
proxy->x() == leftEdgeX && proxy->y() > topEdgeY,
"drag preview keeps moving vertically along a clamped left edge"
)
&& ok;

source->inputArea()->dispatchMotion(overlay.width() + 100.0f, overlay.height() + 100.0f);
bounds = previewBounds();
ok = expect(
bounds.x + bounds.width <= overlay.width() + 0.001f
&& bounds.y + bounds.height <= overlay.height() + 0.001f,
"drag preview stays inside the bottom-right edge"
)
&& ok;

const float rightEdgeX = proxy->x();
const float bottomEdgeY = proxy->y();
source->inputArea()->dispatchMotion(80.0f, overlay.height() + 100.0f);
ok = expect(
proxy->x() < rightEdgeX && proxy->y() == bottomEdgeY,
"drag preview keeps moving horizontally along a clamped bottom edge"
)
&& ok;

// A preview larger than the overlay cannot fit inside it. It covers the
// overlay instead, and keeps tracking the pointer within that range.
overlay.setSize(60.0f, 20.0f);
source->inputArea()->dispatchMotion(0.0f, 0.0f);
bounds = previewBounds();
const float coveringX = proxy->x();
ok = expect(
bounds.x <= 0.001f
&& bounds.y <= 0.001f
&& bounds.x + bounds.width >= overlay.width() - 0.001f
&& bounds.y + bounds.height >= overlay.height() - 0.001f,
"an oversized drag preview keeps covering the overlay"
)
&& ok;

source->inputArea()->dispatchMotion(-30.0f, 0.0f);
bounds = previewBounds();
ok = expect(
proxy->x() < coveringX && bounds.x + bounds.width >= overlay.width() - 0.001f,
"an oversized drag preview still follows the pointer while covering the overlay"
)
&& ok;
overlay.setSize(400.0f, 160.0f);
}

source->inputArea()->dispatchPress(localX, localY, BTN_LEFT, false);
ok = expect(overlay.children().empty(), "drop removes drag preview before callback rerender") && ok;
ok = expect(
Expand Down