Skip to content

Anchors now adjust to size & scale changes - #1317

Open
GeneralProtectionFault wants to merge 1 commit into
Redot-Engine:masterfrom
GeneralProtectionFault:anchor_scale_fix
Open

Anchors now adjust to size & scale changes#1317
GeneralProtectionFault wants to merge 1 commit into
Redot-Engine:masterfrom
GeneralProtectionFault:anchor_scale_fix

Conversation

@GeneralProtectionFault

@GeneralProtectionFault GeneralProtectionFault commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Fixes godotengine/godot#19068

The problem described was really 1 piece of additional issues. The anchor presets simply did not take scale or size into account. Additionally (a natural consequence), adjusting those properties did not update per the anchor preset. This PR addresses all of that:

  • Anchor preset now takes scale into account
  • Anchor preset now takes size into account
  • Adjusting either size or scale will dynamically update the placement of the object so it is correct for the preset.
  • Separated the preset position calculation to its own function (helps separate because setting the size without careful planning could fire a messy infinite loop of _size_changed() signals).
  • Position x & y fields are grayed out (PROPERTY_USAGE_READ_ONLY) when Layout Mode is Anchors Preset, because it makes no sense to change that if the position is determined by the anchor preset.
  • The user can still drag with the mouse and change the position. Disabling that seemed a little invasive, but could probably be added if desired.

I didn't try to do this with rotation because the expected behavior is ambiguous, plus, ugh...

Summary by CodeRabbit

Bug Fixes

  • Improved alignment for anchor-based layouts when scaling, resizing, or changing pivot offsets.
  • Fixed preset offset recalculation so anchors remain consistent after control size changes.
  • Prevented direct position editing and position-setting in anchor-based layout modes.
  • Improved anchor preset handling, including margin tracking and immediate offset updates when presets are applied.

Documentation

  • Clarified anchor-side values, offset ordering, preset resize behavior, edge-gap margins, and right-to-left layout interactions.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • ✅ Review completed - (🔄 Check again to review again)

Walkthrough

Control now uses scale- and pivot-aware preset offsets, reapplies them when size or transforms change, and marks position read-only for anchors-based layouts in the editor.

Changes

Control layout preset alignment

Layer / File(s) Summary
Preset offset computation and application
scene/gui/control.h, scene/gui/control.cpp
Adds internal preset-tracking state and a table-driven offset calculation helper using pivot, scale, size, and margin values for preset alignment.
Anchor-layout size and transform updates
scene/gui/control.cpp
Recomputes preset offsets after size, scale, and pivot changes, clears tracking when switching to position layout, and makes position read-only in anchors-based editor layouts.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: anchor presets now adapt to size and scale changes.
Linked Issues check ✅ Passed The changes address #19068 by making anchor-based placement recompute with scale-aware offsets.
Out of Scope Changes check ✅ Passed The additional read-only position and manual positioning restrictions are consistent with the stated anchor-preset behavior.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@GeneralProtectionFault

Copy link
Copy Markdown
Contributor Author

align-scaled-issue.zip
Attaching the test project that was provided in the upstream issue.
This one is after me toying around with it and after saving it in the Redot editor.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
scene/gui/control.cpp (1)

1565-1567: 📐 Maintainability & Code Quality | 🔵 Trivial

Repeated current_preset <= (int)PRESET_CENTER magic-number check across 3 functions.

The corner/center-vs-wide split relies on enum ordering (<= PRESET_CENTER) duplicated in set_size(), set_scale(), and set_pivot_offset(). Given the case-label mix-ups already found in _compute_preset_offsets(), a small shared helper (e.g. _is_wide_preset(preset)) would reduce the risk of this enum-ordering assumption silently breaking again if presets are reordered.

Also applies to: 1646-1649, 1701-1704

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scene/gui/control.cpp` around lines 1565 - 1567, Replace the duplicated
current_preset <= (int)PRESET_CENTER checks in set_size(), set_scale(), and
set_pivot_offset() with a shared helper such as _is_wide_preset(). Implement the
helper using the existing preset classification rule and use it consistently in
all three functions, preserving their current behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scene/gui/control.cpp`:
- Around line 1696-1706: The anchor-preset reapplication in set_pivot_offset()
must preserve existing margins and control size, avoiding the margin loss and
wide-preset MINSIZE reset also present in set_scale(). Adjust this path to use
the same corrected preset-handling behavior as set_scale(), while retaining
support for LAYOUT_MODE_ANCHORS and LAYOUT_MODE_UNCONTROLLED.
- Around line 1560-1571: Update the preset reapplication logic in set_size() to
retain and pass the margin previously configured by set_offsets_preset() when
calling _compute_preset_offsets(), rather than using the default margin. Also
apply this automatic preset repositioning for both LAYOUT_MODE_ANCHORS and
LAYOUT_MODE_UNCONTROLLED, while preserving the existing preset filtering and
resize notification behavior.
- Around line 1641-1651: Update the anchor-preset handling in set_scale() to
match set_size(): preserve existing margins, include LAYOUT_MODE_UNCONTROLLED
where required, and avoid reapplying wide/full presets that would resize custom
dimensions via PRESET_MODE_MINSIZE. Reapply only the safe presets and modes
needed to maintain visual alignment without changing the control’s user-defined
size.

---

Nitpick comments:
In `@scene/gui/control.cpp`:
- Around line 1565-1567: Replace the duplicated current_preset <=
(int)PRESET_CENTER checks in set_size(), set_scale(), and set_pivot_offset()
with a shared helper such as _is_wide_preset(). Implement the helper using the
existing preset classification rule and use it consistently in all three
functions, preserving their current behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f185050f-66d9-4a99-afef-83f5b320c87e

📥 Commits

Reviewing files that changed from the base of the PR and between d890d7d and fc91851.

📒 Files selected for processing (2)
  • scene/gui/control.cpp
  • scene/gui/control.h

Comment thread scene/gui/control.cpp Outdated
Comment thread scene/gui/control.cpp
Comment thread scene/gui/control.cpp
@GeneralProtectionFault
GeneralProtectionFault marked this pull request as draft July 20, 2026 05:56
@GeneralProtectionFault
GeneralProtectionFault marked this pull request as ready for review July 21, 2026 05:31

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
scene/gui/control.cpp (1)

1645-1654: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate anchor-preset-reapply block in set_scale() and set_pivot_offset().

Both blocks are identical: guard on is_inside_tree() && data.anchor_preset_active, resolve current_preset, and call _compute_preset_offsets(..., get_size(), data.anchor_preset_margin) + _size_changed(). Past review rounds already had to fix the same bug (margin loss, MINSIZE clobbering) independently in both spots — extracting a shared helper would prevent this class of divergence from recurring.

♻️ Suggested helper
void Control::_reapply_anchor_preset_offsets() {
	if (!is_inside_tree() || !data.anchor_preset_active) {
		return;
	}
	int current_preset = _get_anchors_layout_preset();
	if (current_preset == -1) {
		return;
	}
	_compute_preset_offsets((LayoutPreset)current_preset, get_size(), data.anchor_preset_margin);
	_size_changed();
}

Then both set_scale() and set_pivot_offset() reduce to a single call to _reapply_anchor_preset_offsets();.

Also applies to: 1699-1708

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scene/gui/control.cpp` around lines 1645 - 1654, Extract the duplicated
anchor-preset reapplication logic from set_scale() and set_pivot_offset() into a
shared Control helper, such as _reapply_anchor_preset_offsets(). Have it perform
the existing tree/active guard, preset lookup, offset computation, and
_size_changed() call, then replace both inline blocks with a single helper call.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scene/gui/control.cpp`:
- Around line 1254-1260: Fix the RTL width calculation in
Control::_compute_preset_offsets so x remains the parent rectangle’s actual
width in both layout directions; do not mirror or reassign it using p_size.x.
Preserve the existing preset formulas, including the stretching-axis cases such
as PRESET_TOP_WIDE, PRESET_BOTTOM_WIDE, PRESET_HCENTER_WIDE, and
PRESET_FULL_RECT, so they divide and offset against parent_rect.size.x rather
than -p_size.x.
- Around line 1233-1240: Update Control::set_offsets_preset() so applying a
preset does not leave data.anchor_preset_active enabled for subsequent geometry
changes. Preserve the immediate preset application while ensuring later
set_size(), set_scale(), set_pivot_offset(), or manual set_offset() calls are
not overridden by stale preset state.
- Around line 606-609: Update the property-usage logic around the use_anchors
check in the Control property list so position is marked
PROPERTY_USAGE_READ_ONLY for parentless Controls whenever anchor-based layout is
active, including the no-parent editor branch. Keep the existing anchored
behavior for parented Controls and ensure the anchors_preset/anchor_* properties
remain exposed consistently.

---

Nitpick comments:
In `@scene/gui/control.cpp`:
- Around line 1645-1654: Extract the duplicated anchor-preset reapplication
logic from set_scale() and set_pivot_offset() into a shared Control helper, such
as _reapply_anchor_preset_offsets(). Have it perform the existing tree/active
guard, preset lookup, offset computation, and _size_changed() call, then replace
both inline blocks with a single helper call.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 13d16e2e-b2ab-494c-87ff-57168b1a6705

📥 Commits

Reviewing files that changed from the base of the PR and between a1c1d0f and 004fc5a.

📒 Files selected for processing (2)
  • scene/gui/control.cpp
  • scene/gui/control.h
🚧 Files skipped from review as they are similar to previous changes (1)
  • scene/gui/control.h

Comment thread scene/gui/control.cpp
Comment thread scene/gui/control.cpp
Comment thread scene/gui/control.cpp Outdated
@GeneralProtectionFault

Copy link
Copy Markdown
Contributor Author

Hmm, mulling this, auto-updating the "anchored" control makes sense, but it does raise the thought of the API itself, and if this affects calling a change of position. Could probably add the is_in_editor() hint, but wouldn't want to confuse the user.

@Arctis-Fireblight

Copy link
Copy Markdown
Contributor

I am adding the "breaks compatibility" label to this one.

This has been a long standing problem / behavior in Godot for ages.

My thought process is, that anchoring is a pretty core feature in UI design, and countless projects are using this feature in Godot and Redot.

The existing behavior is obviously incorrect and should be fixed, but this is something I feel a lot of users will have ran into and adjusted their code and UIs to workaround. We'll need to make sure that once this is merged, that we make sure that this is clearly notated in the release notes, and we will need to make sure that we make mention of this in the eventual migration guide between 26.2 and 26.3.

@Shakai-Dev

Copy link
Copy Markdown
Contributor

I am adding the "breaks compatibility" label to this one.

This has been a long standing problem / behavior in Godot for ages.

My thought process is, that anchoring is a pretty core feature in UI design, and countless projects are using this feature in Godot and Redot.

The existing behavior is obviously incorrect and should be fixed, but this is something I feel a lot of users will have ran into and adjusted their code and UIs to workaround. We'll need to make sure that once this is merged, that we make sure that this is clearly notated in the release notes, and we will need to make sure that we make mention of this in the eventual migration guide between 26.2 and 26.3.

I agree with you, this does indeed break compat with Godot & older versions of Redot but since we're becoming a hard fork anyways, mentioning this in the release notes may work but people often don't read that sorta stuff. Maybe we could add a warning for projects migrating from older versions of Redot to 26.3 to let them know that this version breaks compatibility?

@VictorSohier VictorSohier left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The requested changes aren't mandatory, but they'd make this file a lot easier to read and might also be a slight performance gain. Otherwise, the changes look inoffensive.

Comment thread scene/gui/control.cpp Outdated
Comment on lines 1268 to 1343
@@ -1279,26 +1308,37 @@ void Control::set_offsets_preset(LayoutPreset p_preset, LayoutPresetMode p_resiz
case PRESET_TOP_LEFT:
case PRESET_TOP_RIGHT:
case PRESET_CENTER_TOP:
case PRESET_LEFT_WIDE:
case PRESET_RIGHT_WIDE:
case PRESET_TOP_WIDE:
case PRESET_VCENTER_WIDE:
case PRESET_FULL_RECT:
data.offset[1] = parent_rect.size.y * (0.0 - data.anchor[1]) + p_margin + parent_rect.position.y;
// Fixed-size axis: visual top edge at p_margin.
data.offset[1] = parent_rect.size.y * (0.0 - data.anchor[1]) + pivot_corr_y + p_preset_margin + parent_rect.position.y;
break;

case PRESET_BOTTOM_WIDE:
// Fixed-size axis: visual top edge at parent_bottom - scaled_height - margin.
data.offset[1] = parent_rect.size.y * (1.0 - data.anchor[1]) + pivot_corr_y - scaled_height - p_preset_margin + parent_rect.position.y;
break;

case PRESET_CENTER_LEFT:
case PRESET_CENTER_RIGHT:
case PRESET_CENTER:
case PRESET_HCENTER_WIDE:
data.offset[1] = parent_rect.size.y * (0.5 - data.anchor[1]) - new_size.y / 2 + parent_rect.position.y;
// Fixed-size axis: visual center at parent vertical center.
data.offset[1] = parent_rect.size.y * (0.5 - data.anchor[1]) + pivot_corr_y - scaled_height / 2.0f + parent_rect.position.y;
break;

case PRESET_BOTTOM_LEFT:
case PRESET_BOTTOM_RIGHT:
case PRESET_CENTER_BOTTOM:
case PRESET_BOTTOM_WIDE:
data.offset[1] = parent_rect.size.y * (1.0 - data.anchor[1]) - new_size.y - p_margin + parent_rect.position.y;
// Fixed-size axis: visual bottom edge at parent_bottom - margin.
data.offset[1] = parent_rect.size.y * (1.0 - data.anchor[1]) + pivot_corr_y - scaled_height - p_preset_margin + parent_rect.position.y;
break;

case PRESET_LEFT_WIDE:
case PRESET_RIGHT_WIDE:
case PRESET_VCENTER_WIDE:
case PRESET_FULL_RECT:
// Stretching axis: visual top edge at margin, accounting for pivot.
data.offset[1] = parent_rect.size.y * (0.0 - data.anchor[1]) + pivot_corr_y + p_preset_margin + parent_rect.position.y;
break;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be preferable to turn this into either a table, or a table that indexes to another table while you're at it

Comment thread scene/gui/control.cpp Outdated
Comment on lines 1348 to 1415
@@ -1336,29 +1384,35 @@ void Control::set_offsets_preset(LayoutPreset p_preset, LayoutPresetMode p_resiz
case PRESET_TOP_RIGHT:
case PRESET_CENTER_TOP:
case PRESET_TOP_WIDE:
data.offset[3] = parent_rect.size.y * (0.0 - data.anchor[3]) + new_size.y + p_margin + parent_rect.position.y;
// Fixed-size axis: bottom = logical_top + p_size.y.
data.offset[3] = parent_rect.size.y * (0.0 - data.anchor[3]) + pivot_corr_y + p_size.y + p_preset_margin + parent_rect.position.y;
break;

case PRESET_CENTER_LEFT:
case PRESET_CENTER_RIGHT:
case PRESET_CENTER:
case PRESET_HCENTER_WIDE:
data.offset[3] = parent_rect.size.y * (0.5 - data.anchor[3]) + new_size.y / 2 + parent_rect.position.y;
// Fixed-size axis: centered.
data.offset[3] = parent_rect.size.y * (0.5 - data.anchor[3]) + pivot_corr_y + p_size.y - scaled_height / 2.0f + parent_rect.position.y;
break;

case PRESET_BOTTOM_LEFT:
case PRESET_BOTTOM_RIGHT:
case PRESET_CENTER_BOTTOM:
case PRESET_BOTTOM_WIDE:
// Fixed-size axis: bottom edge at parent_bottom - margin.
data.offset[3] = parent_rect.size.y * (1.0 - data.anchor[3]) + pivot_corr_y + p_size.y * (1.0f - data.scale.y) - p_preset_margin + parent_rect.position.y;
break;

case PRESET_LEFT_WIDE:
case PRESET_RIGHT_WIDE:
case PRESET_BOTTOM_WIDE:
case PRESET_VCENTER_WIDE:
case PRESET_FULL_RECT:
data.offset[3] = parent_rect.size.y * (1.0 - data.anchor[3]) - p_margin + parent_rect.position.y;
// Stretching axis: visual bottom edge at parent_bottom - margin.
// Divide by scale so visual_height = parent_height - 2*margin regardless of scale.
data.offset[3] = pivot_corr_y + p_preset_margin + (parent_rect.size.y - 2.0f * p_preset_margin) / data.scale.y - parent_rect.size.y + parent_rect.position.y;
break;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same with this

@Shakai-Dev Shakai-Dev added this to the Redot LTS 26.3 milestone Jul 23, 2026
@GeneralProtectionFault
GeneralProtectionFault force-pushed the anchor_scale_fix branch 2 times, most recently from d6767c5 to a3b09f9 Compare July 26, 2026 02:34

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (4)
scene/gui/control.h (3)

46-47: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Include set doesn't match usage.

<map> isn't used by any of the added code, while <array> and <tuple> (needed for std::array / std::tuple at Lines 405-408) are only reaching this TU transitively.

♻️ Proposed fix
+#include <array>
 `#include` <functional>
-#include <map>
+#include <tuple>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scene/gui/control.h` around lines 46 - 47, Update the include list in
control.h: remove the unused <map> include and add direct includes for <array>
and <tuple>, which are required by the std::array and std::tuple usages in the
added code.

316-333: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

current_offset is never read.

None of the 12 calculators reference p.current_offset, yet _compute_preset_offsets() populates it via std::copy on every call (scene/gui/control.cpp Line 1174). Drop the field and the copy unless a future calculator needs it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scene/gui/control.h` around lines 316 - 333, Remove the unused current_offset
field from OffsetCalcParams and delete the corresponding std::copy population in
_compute_preset_offsets(). Ensure the remaining offset calculators and parameter
initialization continue using only the fields they reference.

402-425: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use plain function pointers and make the table constexpr.

Every entry is a stateless free function, so std::function buys nothing here and costs a type-erased indirect call plus a non-trivial global constructor. It also leaves anchor_preset_calc_table as mutable global state (it's inline static and non-const), writable from anywhere. A raw function pointer alias makes the whole table constexpr, immutable, and constant-initialized.

♻️ Proposed refactor
-	using OffsetCalculator = std::function<real_t(const OffsetCalcParams &)>;
+	using OffsetCalculator = real_t (*)(const OffsetCalcParams &);
 
 	// Each OffsetCalculator holds a function to calculate offset: LEFT, TOP, RIGHT, BOTTOM
-	inline static std::array<std::tuple<OffsetCalculator, OffsetCalculator, OffsetCalculator, OffsetCalculator>,
+	static constexpr std::array<std::tuple<OffsetCalculator, OffsetCalculator, OffsetCalculator, OffsetCalculator>,
 			16 // MUST be kept the size of the LayoutPreset enum
 			>
 			anchor_preset_calc_table = { {

With this change <functional> is no longer needed either.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scene/gui/control.h` around lines 402 - 425, Replace the std::function-based
OffsetCalculator alias with a plain function-pointer type matching the existing
stateless offset helpers, then declare anchor_preset_calc_table as constexpr so
it is immutable and constant-initialized. Preserve all table entries and
ordering, and remove the now-unused functional header.
scene/gui/control.cpp (1)

1064-1093: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer a constexpr table over the lambda-initialized static.

The lookup contents are compile-time constants; the immediately-invoked lambda adds a guarded first-call initialization and returns a pointer to a function-local array for no benefit. A static constexpr int PRESET_LOOKUP[256] with designated or explicitly-indexed initialization (or a small switch on key) is simpler and constant-folded.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scene/gui/control.cpp` around lines 1064 - 1093, Replace the
lambda-initialized static PRESET_LOOKUP in the preset lookup logic with a
directly declared static constexpr table or an equivalent small switch on key.
Preserve every existing valid LayoutPreset mapping and return -1 for all invalid
or custom keys without runtime initialization.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scene/gui/control.cpp`:
- Around line 1333-1348: Align the comment in the resize logic with the behavior
of the preset branch around _get_anchors_layout_preset and
_compute_preset_offsets: either restrict preset_applied to exclude wide/full
presets as described, or remove the inaccurate statement that they are skipped.
Ensure the final comment accurately reflects how PRESET_FULL_RECT and other wide
presets are handled.
- Around line 1044-1052: Change _get_anchors_layout_preset() to return a
distinct “no preset” sentinel for non-anchor layout modes instead of
PRESET_TOP_LEFT, preserving genuine top-left preset detection. Update all
consumers—_get_layout_mode(), _validate_property(), and the reapply paths in
set_size(), set_scale(), and set_pivot_offset()—to recognize the new sentinel
and use the pos_cache/normal fallback without applying a preset.
- Around line 1264-1265: Update Control::set_position() so the ERR_FAIL_COND_MSG
check for LayoutMode::LAYOUT_MODE_ANCHORS is enforced only when the editor
anchoring mode is active, rather than for runtime scripting calls. Ensure
runtime assignments still update the position and remain observable through
get_position(), while preserving the existing editor warning/blocking behavior.
- Around line 1054-1062: Update the local encode lambda used to build key so it
returns preset bucket values only when the anchor is approximately equal to 0.0,
0.5, or 1.0; return the existing custom-anchor sentinel for every other value,
including NaN. Preserve the bit-packing order in key and use the project’s
established approximate-equality helper.

In `@scene/gui/control.h`:
- Around line 378-380: Update _calculate_right_offset_wide and the corresponding
wide-preset calculation around the referenced second location to guard against
near-zero scale_x before division. Clamp the divisor magnitude or use the prior
scale-independent formula when its absolute value is below a sane threshold,
while preserving valid-scale behavior and avoiding unintended negative-scale
flipping.

---

Nitpick comments:
In `@scene/gui/control.cpp`:
- Around line 1064-1093: Replace the lambda-initialized static PRESET_LOOKUP in
the preset lookup logic with a directly declared static constexpr table or an
equivalent small switch on key. Preserve every existing valid LayoutPreset
mapping and return -1 for all invalid or custom keys without runtime
initialization.

In `@scene/gui/control.h`:
- Around line 46-47: Update the include list in control.h: remove the unused
<map> include and add direct includes for <array> and <tuple>, which are
required by the std::array and std::tuple usages in the added code.
- Around line 316-333: Remove the unused current_offset field from
OffsetCalcParams and delete the corresponding std::copy population in
_compute_preset_offsets(). Ensure the remaining offset calculators and parameter
initialization continue using only the fields they reference.
- Around line 402-425: Replace the std::function-based OffsetCalculator alias
with a plain function-pointer type matching the existing stateless offset
helpers, then declare anchor_preset_calc_table as constexpr so it is immutable
and constant-initialized. Preserve all table entries and ordering, and remove
the now-unused functional header.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 06d9676d-1e05-4afd-ab22-2f6900f1cdc5

📥 Commits

Reviewing files that changed from the base of the PR and between 004fc5a and d6767c5.

📒 Files selected for processing (2)
  • scene/gui/control.cpp
  • scene/gui/control.h

Comment thread scene/gui/control.cpp
Comment thread scene/gui/control.cpp
Comment thread scene/gui/control.cpp
Comment thread scene/gui/control.cpp
Comment thread scene/gui/control.h
Comment on lines +378 to +380
static real_t _calculate_right_offset_wide(const OffsetCalcParams &p) {
return p.pivot_x_correction + p.p_preset_margin + (p.parent_x_size - 2.0f * p.p_preset_margin) / p.scale_x - p.parent_x_size + p.parent_x_position;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Wide presets now divide by scale, which explodes at near-zero scale.

set_scale() clamps a zero component to CMP_EPSILON rather than rejecting it, so set_scale(Vector2(0, 1)) is a legal call that makes (p.parent_x_size - 2.0f * p.p_preset_margin) / p.scale_x produce ~1e19 and poison data.offset[2]/size_cache for the rest of the frame. Pre-PR the wide offsets were scale-independent, so this is newly reachable. Negative scale flips the rect as well.

Consider clamping the divisor magnitude (or bailing out to the unscaled formula) when |scale| is below a sane threshold.

Also applies to: 397-399

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scene/gui/control.h` around lines 378 - 380, Update
_calculate_right_offset_wide and the corresponding wide-preset calculation
around the referenced second location to guard against near-zero scale_x before
division. Clamp the divisor magnitude or use the prior scale-independent formula
when its absolute value is below a sane threshold, while preserving valid-scale
behavior and avoiding unintended negative-scale flipping.

Comment thread scene/gui/control.cpp Outdated
Comment on lines +1056 to +1059
auto encode = [](real_t val) {
return (val < 0.25f) ? 0 : (val < 0.75f) ? 1
: 2;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Somewhat of a preference thing, but also, the rest of the engine doesn't use auto. Might be a good idea to statically type things.

Comment thread scene/gui/control.h Outdated
Comment on lines +405 to +425
inline static std::array<std::tuple<OffsetCalculator, OffsetCalculator, OffsetCalculator, OffsetCalculator>,
16 // MUST be kept the size of the LayoutPreset enum
>
anchor_preset_calc_table = { {
{ _calculate_left_offset_left_justified, _calculate_top_offset_top_justified, _calculate_right_offset_left_justified, _calculate_bottom_offset_top_justified }, // PRESET_TOP_LEFT
{ _calculate_left_offset_right_justified, _calculate_top_offset_top_justified, _calculate_right_offset_right_justified, _calculate_bottom_offset_top_justified }, // PRESET_TOP_RIGHT
{ _calculate_left_offset_left_justified, _calculate_top_offset_bottom_justified, _calculate_right_offset_left_justified, _calculate_bottom_offset_bottom_justified }, // PRESET_BOTTOM_LEFT
{ _calculate_left_offset_right_justified, _calculate_top_offset_bottom_justified, _calculate_right_offset_right_justified, _calculate_bottom_offset_bottom_justified }, // PRESET_BOTTOM_RIGHT
{ _calculate_left_offset_left_justified, _calculate_top_offset_center_justified, _calculate_right_offset_left_justified, _calculate_bottom_offset_center_justified }, // PRESET_CENTER_LEFT
{ _calculate_left_offset_center_justified, _calculate_top_offset_top_justified, _calculate_right_offset_center_justified, _calculate_bottom_offset_top_justified }, // PRESET_CENTER_TOP
{ _calculate_left_offset_right_justified, _calculate_top_offset_center_justified, _calculate_right_offset_right_justified, _calculate_bottom_offset_center_justified }, // PRESET_CENTER_RIGHT
{ _calculate_left_offset_center_justified, _calculate_top_offset_bottom_justified, _calculate_right_offset_center_justified, _calculate_bottom_offset_bottom_justified }, // PRESET_CENTER_BOTTOM
{ _calculate_left_offset_center_justified, _calculate_top_offset_center_justified, _calculate_right_offset_center_justified, _calculate_bottom_offset_center_justified }, // PRESET_CENTER
{ _calculate_left_offset_left_justified, _calculate_top_offset_top_justified, _calculate_right_offset_left_justified, _calculate_bottom_offset_wide }, // PRESET_LEFT_WIDE
{ _calculate_left_offset_left_justified, _calculate_top_offset_top_justified, _calculate_right_offset_wide, _calculate_bottom_offset_top_justified }, // PRESET_TOP_WIDE
{ _calculate_left_offset_right_justified, _calculate_top_offset_top_justified, _calculate_right_offset_right_justified, _calculate_bottom_offset_wide }, // PRESET_RIGHT_WIDE
{ _calculate_left_offset_left_justified, _calculate_top_offset_bottom_justified, _calculate_right_offset_wide, _calculate_bottom_offset_bottom_justified }, // PRESET_BOTTOM_WIDE
{ _calculate_left_offset_center_justified, _calculate_top_offset_top_justified, _calculate_right_offset_center_justified, _calculate_bottom_offset_wide }, // PRESET_VCENTER_WIDE
{ _calculate_left_offset_left_justified, _calculate_top_offset_center_justified, _calculate_right_offset_wide, _calculate_bottom_offset_center_justified }, // PRESET_HCENTER_WIDE
{ _calculate_left_offset_left_justified, _calculate_top_offset_top_justified, _calculate_right_offset_wide, _calculate_bottom_offset_wide } // PRESET_FULL_RECT
} };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll just point out that if all you are doing is dispatching to functions, you lose if they would've been inlined. It might be better to just compute all the possible results in a table and copy from there. CPUs are great at computing quite a few independent results simultaneously. There are only 14 entries here, if you wanted to ensure that you only do those 14 computations, have another table of indices that index into the computed table then.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I believe we do need the calculation to stay. The engine supports custom anchor presets, so the values, themselves, can theoretically be anything. Additionally, I think the functions, while a little nauseating, kind of self document how the work is being done.

But, I'll see if there's a way to get rid of the function calling overhead.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can perform the calculation in the array declaration in the function. Pipelining should be able to take care of it if it's just 14 calculations with 8 ops. This is one thing compilers are good at managing.

@GeneralProtectionFault

GeneralProtectionFault commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Think this is cleaned up pretty well. EDIT...besides the failed unit tests xD...tomorrow...
--I have a hunch that purposely disallowing the position update when an anchor preset is active might be why the unit tests are being like aw, hell nah--

Posterity note: I came across what I think is a separate bug -- When changing layout direction to right-to-left, it horizontally mirrors the control node, regardless of the anchor preset.
My understanding is this mode is meant for languages that read right to left, but in the set_size() function, the x mirror is applied indiscriminately to the control node. I think the anchor preset is something that needs to be excluded from this, as it's positional, not semantic.
I added a @todo to the efffect. I'll probably add an issue as well, but I think this bears a separate PR.

@GeneralProtectionFault

Copy link
Copy Markdown
Contributor Author

align-scaled-issue-2.zip
I'm attaching a slightly modified version of the previously attached example project. This one adds a little script to the icon, purposely trying to set the position when the anchor preset is established. This is confirmed disallowed and error thrown which can be seen in the Debugging window at the bottom.

Additionally, it manipulates one of the anchor offsets in code, which squishes the icon. In line with the warning text, overriding the offset values, themselves, is what makes sense when an anchor preset is used, not the position--because the anchor offset manipulates the position.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
scene/gui/control.h (4)

405-421: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Undefine LAYOUT_PRESET_LIST after use.

X is undefined at line 436, but LAYOUT_PRESET_LIST stays defined. control.h is included by a large part of the engine, so this macro leaks into every consuming translation unit and can collide with other definitions. Add #undef LAYOUT_PRESET_LIST after the switch.

Also consider a default: label in the switch. _set_anchors_layout_preset() casts an int into LayoutPreset, so an out-of-range value currently leaves data.offset untouched with no diagnostic.

♻️ Proposed cleanup
 			LAYOUT_PRESET_LIST
 `#undef` X
+			default:
+				ERR_FAIL_MSG("Unknown layout preset.");
 		}
 	}
+#undef LAYOUT_PRESET_LIST

Also applies to: 435-437

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scene/gui/control.h` around lines 405 - 421, Undefine LAYOUT_PRESET_LIST
immediately after its final expansion in the switch so the helper macro does not
leak from control.h; also add a default label to _set_anchors_layout_preset() to
handle out-of-range LayoutPreset values with an explicit diagnostic or safe
fallback, while preserving existing behavior for valid presets.

326-330: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rename the p_-prefixed struct members.

In this codebase p_ marks function parameters. p_size_x, p_size_y, and p_preset_margin are struct members, so the prefix is misleading. Use size_x, size_y, and preset_margin, and update the designated initializers in _compute_preset_offsets() and the calculator functions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scene/gui/control.h` around lines 326 - 330, Rename the struct members
p_size_x, p_size_y, and p_preset_margin to size_x, size_y, and preset_margin,
respectively, and update all designated initializers and member references in
_compute_preset_offsets() and the related calculator functions to use the new
names.

318-319: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Drop current_offset from OffsetCalcParams.

No calculator function reads current_offset. _compute_preset_offsets() in scene/gui/control.cpp still copies data.offset into it on every call, which is dead work in a layout path that runs on each size, scale, and pivot change. Remove the field and the copy, or add the calculator that consumes it.

anchor_position is used (indices 0-3), so keep that field.

♻️ Proposed cleanup
 	struct OffsetCalcParams {
-		real_t current_offset[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; ///< The value the offset is to start with
 		real_t anchor_position[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; ///< 0.0 - 1.0

Then remove the matching copy in scene/gui/control.cpp:

-	std::copy(std::begin(data.offset), std::end(data.offset), std::begin(params.current_offset));
 	std::copy(std::begin(data.anchor), std::end(data.anchor), std::begin(params.anchor_position));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scene/gui/control.h` around lines 318 - 319, Remove the unused current_offset
field from OffsetCalcParams while preserving anchor_position. Update
_compute_preset_offsets() to stop copying data.offset into current_offset,
unless a calculator is added that actually consumes it.

46-47: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unused <functional> and <map> includes.

control.h does not use std::function, std::map, or std::multimap; the copy operations in control.cpp need <algorithm>, not these headers. Remove them to avoid adding unnecessary compile dependency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scene/gui/control.h` around lines 46 - 47, Remove the unused <functional> and
<map> includes from control.h, leaving the required header for copy operations
in control.cpp unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@scene/gui/control.h`:
- Around line 405-421: Undefine LAYOUT_PRESET_LIST immediately after its final
expansion in the switch so the helper macro does not leak from control.h; also
add a default label to _set_anchors_layout_preset() to handle out-of-range
LayoutPreset values with an explicit diagnostic or safe fallback, while
preserving existing behavior for valid presets.
- Around line 326-330: Rename the struct members p_size_x, p_size_y, and
p_preset_margin to size_x, size_y, and preset_margin, respectively, and update
all designated initializers and member references in _compute_preset_offsets()
and the related calculator functions to use the new names.
- Around line 318-319: Remove the unused current_offset field from
OffsetCalcParams while preserving anchor_position. Update
_compute_preset_offsets() to stop copying data.offset into current_offset,
unless a calculator is added that actually consumes it.
- Around line 46-47: Remove the unused <functional> and <map> includes from
control.h, leaving the required header for copy operations in control.cpp
unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 96f1f933-04f5-4e14-b3d4-cb65e384b2da

📥 Commits

Reviewing files that changed from the base of the PR and between d6767c5 and 3699a54.

📒 Files selected for processing (2)
  • scene/gui/control.cpp
  • scene/gui/control.h
🚧 Files skipped from review as they are similar to previous changes (1)
  • scene/gui/control.cpp

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Open

Development

Successfully merging this pull request may close these issues.

Scaled Controls don't align properly

4 participants