Browse Source

Fix #100536: Control set_position resizes offsets/anchors
The set_position method computes the anchors/offsets to match a
rectangle at the given position with size "size_cache". However, when
the Control's combined minimum size is larger than the size obtained
through the offsets and anchors, "size_cache" is set to be as large as
the combined minimum size. Therefore, when position is set while the
combined minimum size is larger than the rectangle given by the anchors
and offsets, it would resize these two fields, which would then stop
the Control from shrinking when its combined minimum size decreased. To
fix this, set_position now uses the size given by the offsets and
anchors instead of the "size_cache" field. This way, the rectangle
denoted by the offsets and anchors is simply moved, without being
resized, enabling the Control to shrink automatically when its combined
minimum size decreases. I also added a test case to ensure that the
Control shrinks correctly after setting its position while it has a
larger custom minimum size than the one obtained through the offsets
and anchors.

Filipe Alexandre Francisco Costa 4 months ago
parent
commit
ca57fe1db4
3 changed files with 46 additions and 9 deletions
  1. 13 9
      scene/gui/control.cpp
  2. 1 0
      scene/gui/control.h
  3. 32 0
      tests/scene/test_control.h

+ 13 - 9
scene/gui/control.cpp

@@ -867,6 +867,13 @@ void Control::_compute_offsets(Rect2 p_rect, const real_t p_anchors[4], real_t (
 	r_offsets[3] = p_rect.position.y + p_rect.size.y - (p_anchors[3] * parent_rect_size.y);
 }
 
+void Control::_compute_edge_positions(Rect2 p_rect, real_t (&r_edge_positions)[4]) {
+	for (int i = 0; i < 4; i++) {
+		real_t area = p_rect.size[i & 1];
+		r_edge_positions[i] = data.offset[i] + (data.anchor[i] * area);
+	}
+}
+
 /// Presets and layout modes.
 
 void Control::_set_layout_mode(LayoutMode p_mode) {
@@ -1392,10 +1399,13 @@ void Control::set_position(const Point2 &p_point, bool p_keep_offsets) {
 	}
 #endif // TOOLS_ENABLED
 
+	real_t edge_pos[4];
+	_compute_edge_positions(get_parent_anchorable_rect(), edge_pos);
+	Size2 offset_size(edge_pos[2] - edge_pos[0], edge_pos[3] - edge_pos[1]);
 	if (p_keep_offsets) {
-		_compute_anchors(Rect2(p_point, data.size_cache), data.offset, data.anchor);
+		_compute_anchors(Rect2(p_point, offset_size), data.offset, data.anchor);
 	} else {
-		_compute_offsets(Rect2(p_point, data.size_cache), data.anchor, data.offset);
+		_compute_offsets(Rect2(p_point, offset_size), data.anchor, data.offset);
 	}
 	_size_changed();
 }
@@ -1682,14 +1692,8 @@ Size2 Control::get_combined_minimum_size() const {
 
 void Control::_size_changed() {
 	Rect2 parent_rect = get_parent_anchorable_rect();
-
 	real_t edge_pos[4];
-
-	for (int i = 0; i < 4; i++) {
-		real_t area = parent_rect.size[i & 1];
-		edge_pos[i] = data.offset[i] + (data.anchor[i] * area);
-	}
-
+	_compute_edge_positions(parent_rect, edge_pos);
 	Point2 new_pos_cache = Point2(edge_pos[0], edge_pos[1]);
 	Size2 new_size_cache = Point2(edge_pos[2], edge_pos[3]) - new_pos_cache;
 

+ 1 - 0
scene/gui/control.h

@@ -300,6 +300,7 @@ private:
 
 	void _compute_offsets(Rect2 p_rect, const real_t p_anchors[4], real_t (&r_offsets)[4]);
 	void _compute_anchors(Rect2 p_rect, const real_t p_offsets[4], real_t (&r_anchors)[4]);
+	void _compute_edge_positions(Rect2 p_rect, real_t (&r_edge_positions)[4]);
 
 	void _set_layout_mode(LayoutMode p_mode);
 	void _update_layout_mode();

+ 32 - 0
tests/scene/test_control.h

@@ -990,6 +990,38 @@ TEST_CASE("[SceneTree][Control] Anchoring") {
 	memdelete(test_control);
 }
 
+TEST_CASE("[SceneTree][Control] Set position does not cause size side-effects") {
+	Control *test_control = memnew(Control);
+	test_control->set_size(Size2(1, 1));
+	test_control->set_custom_minimum_size(Size2(2, 2));
+	Window *root = SceneTree::get_singleton()->get_root();
+	root->add_child(test_control);
+
+	SUBCASE("Shrinks after setting position and smaller custom minimum size (without keeping offsets)") {
+		test_control->set_position(Point2(10, 10), false);
+		SceneTree::get_singleton()->process(0);
+
+		test_control->set_custom_minimum_size(Size2(0, 0));
+		SceneTree::get_singleton()->process(0);
+		CHECK_MESSAGE(
+				test_control->get_size().is_equal_approx(Vector2(1, 1)),
+				"Should shrink to original size after setting a smaller custom minimum size.");
+	}
+
+	SUBCASE("Shrinks after setting position and smaller custom minimum size (while keeping offsets)") {
+		test_control->set_position(Point2(10, 10), true);
+		SceneTree::get_singleton()->process(0);
+
+		test_control->set_custom_minimum_size(Size2(0, 0));
+		SceneTree::get_singleton()->process(0);
+		CHECK_MESSAGE(
+				test_control->get_size().is_equal_approx(Vector2(1, 1)),
+				"Should shrink to original size after setting a smaller custom minimum size.");
+	}
+
+	memdelete(test_control);
+}
+
 TEST_CASE("[SceneTree][Control] Custom minimum size") {
 	Control *test_control = memnew(Control);
 	test_control->set_custom_minimum_size(Size2(4, 2));