diff --git a/doc/classes/Control.xml b/doc/classes/Control.xml index 87fef6d4eb3..eef5b362287 100644 --- a/doc/classes/Control.xml +++ b/doc/classes/Control.xml @@ -409,6 +409,12 @@ Returns combined minimum size from [member custom_minimum_size] and [method get_minimum_size]. + + + + Returns the combined value of [member pivot_offset] and [member pivot_offset_ratio], in pixels. The ratio is multiplied by the control's size. + + @@ -463,6 +469,11 @@ Returns the offset for the specified [enum Side]. A getter method for [member offset_bottom], [member offset_left], [member offset_right] and [member offset_top]. + + + + + @@ -1084,9 +1095,49 @@ Distance between the node's top edge and its parent control, based on [member anchor_top]. Offsets are often controlled by one or multiple parent [Container] nodes, so you should not modify them manually if your node is a direct child of a [Container]. Offsets update automatically when you move or resize the node. + + If [code]true[/code], applies all offset transform properties. Otherwise, no offset transform is applied and the properties have no effect. + + + Pivot used by [member offset_transform_rotation] and [member offset_transform_scale] in absolute units. + The final pivot position is the combined value of this property and [member offset_transform_pivot_ratio]. + Has no effect unless [member offset_transform_enabled] is [code]true[/code]. + + + Same as [member offset_transform_pivot] but expressed in units relative to the [Control] [member size] where [code]Vector2(0, 0)[/code] is the top-left corner of this control, and [code]Vector2(1, 1)[/code] is its bottom-right corner. + The final pivot position is the combined value of this property and [member offset_transform_pivot]. + Has no effect unless [member offset_transform_enabled] is [code]true[/code]. + + + Position offset in absolute units. The final offset is the combined value of this property and [member offset_transform_position_ratio]. + Has no effect unless [member offset_transform_enabled] is [code]true[/code]. + + + Same as [member offset_transform_position] but expressed in units relative to the [Control] [member size] where [code]Vector2(0, 0)[/code] is the top-left corner of this control, and [code]Vector2(1, 1)[/code] is its bottom-right corner. + The final offset is the combined value of this property and [member offset_transform_position]. + Has no effect unless [member offset_transform_enabled] is [code]true[/code]. + + + Rotation offset. The rotation pivot is defined by [member offset_transform_pivot] and [member offset_transform_pivot_ratio]. + Has no effect unless [member offset_transform_enabled] is [code]true[/code]. + + + Scale offset. The scale pivot is defined by [member offset_transform_pivot] and [member offset_transform_pivot_ratio]. + Has no effect unless [member offset_transform_enabled] is [code]true[/code]. + + + If [code]true[/code], the offset transform is only applied visually and does not affect input. In other words, this Control will still receive input events at its original location before the offset transform is applied. + If [code]false[/code], the entire transform of this Control is affected and input events will register where the Control is visually. + Has no effect unless [member offset_transform_enabled] is [code]true[/code]. + - By default, the node's pivot is its top-left corner. When you change its [member rotation] or [member scale], it will rotate or scale around this pivot. Set this property to [member size] / 2 to pivot around the Control's center. + By default, the node's pivot is its top-left corner. When you change its [member rotation] or [member scale], it will rotate or scale around this pivot. + The actual offset is the combined value of this property and [member pivot_offset_ratio]. + + + Same as [member pivot_offset], but expressed as uniform vector, where [code]Vector2(0, 0)[/code] is the top-left corner of this control, and [code]Vector2(1, 1)[/code] is its bottom-right corner. Set this property to [code]Vector2(0.5, 0.5)[/code] to pivot around this control's center. + The actual offset is the combined value of this property and [member pivot_offset]. The node's position, relative to its containing node. It corresponds to the rectangle's top-left corner. The property is not affected by [member pivot_offset]. diff --git a/editor/scene/canvas_item_editor_plugin.cpp b/editor/scene/canvas_item_editor_plugin.cpp index ff8df60dd4e..321037d1914 100644 --- a/editor/scene/canvas_item_editor_plugin.cpp +++ b/editor/scene/canvas_item_editor_plugin.cpp @@ -4210,17 +4210,18 @@ void CanvasItemEditor::_notification(int p_what) { Control *control = Object::cast_to(ci); if (control) { - real_t anchors[4]; - Vector2 pivot; + Vector2 pivot = control->get_pivot_offset(); + Vector2 pivot_ratio = control->get_pivot_offset_ratio(); - pivot = control->get_pivot_offset(); + real_t anchors[4]; anchors[SIDE_LEFT] = control->get_anchor(SIDE_LEFT); anchors[SIDE_RIGHT] = control->get_anchor(SIDE_RIGHT); anchors[SIDE_TOP] = control->get_anchor(SIDE_TOP); anchors[SIDE_BOTTOM] = control->get_anchor(SIDE_BOTTOM); - if (pivot != se->prev_pivot || anchors[SIDE_LEFT] != se->prev_anchors[SIDE_LEFT] || anchors[SIDE_RIGHT] != se->prev_anchors[SIDE_RIGHT] || anchors[SIDE_TOP] != se->prev_anchors[SIDE_TOP] || anchors[SIDE_BOTTOM] != se->prev_anchors[SIDE_BOTTOM]) { + if (pivot != se->prev_pivot || pivot_ratio != se->prev_pivot_ratio || anchors[SIDE_LEFT] != se->prev_anchors[SIDE_LEFT] || anchors[SIDE_RIGHT] != se->prev_anchors[SIDE_RIGHT] || anchors[SIDE_TOP] != se->prev_anchors[SIDE_TOP] || anchors[SIDE_BOTTOM] != se->prev_anchors[SIDE_BOTTOM]) { se->prev_pivot = pivot; + se->prev_pivot_ratio = pivot_ratio; se->prev_anchors[SIDE_LEFT] = anchors[SIDE_LEFT]; se->prev_anchors[SIDE_RIGHT] = anchors[SIDE_RIGHT]; se->prev_anchors[SIDE_TOP] = anchors[SIDE_TOP]; diff --git a/editor/scene/canvas_item_editor_plugin.h b/editor/scene/canvas_item_editor_plugin.h index 9d635ea5657..8bcb452b203 100644 --- a/editor/scene/canvas_item_editor_plugin.h +++ b/editor/scene/canvas_item_editor_plugin.h @@ -66,6 +66,7 @@ class CanvasItemEditorSelectedItem : public Object { Transform2D prev_xform; Rect2 prev_rect; Vector2 prev_pivot; + Vector2 prev_pivot_ratio; real_t prev_anchors[4] = { (real_t)0.0 }; Transform2D pre_drag_xform; diff --git a/editor/scene/gui/control_editor_plugin.cpp b/editor/scene/gui/control_editor_plugin.cpp index 2d4932abc2c..86add00b7b6 100644 --- a/editor/scene/gui/control_editor_plugin.cpp +++ b/editor/scene/gui/control_editor_plugin.cpp @@ -1160,11 +1160,89 @@ ControlEditorToolbar *ControlEditorToolbar::singleton = nullptr; // Editor plugin. +void ControlOffsetTransformPreview::edit(Control *p_control) { + ObjectID new_control_id = p_control ? p_control->get_instance_id() : ObjectID(); + if (new_control_id == selected_control_id) { + return; + } + + const Callable update_overlays = callable_mp(plugin, &EditorPlugin::update_overlays); + + Control *selected_control = ObjectDB::get_instance(selected_control_id); + if (selected_control) { + selected_control->disconnect(SceneStringName(draw), update_overlays); + } + + selected_control_id = new_control_id; + + selected_control = ObjectDB::get_instance(selected_control_id); + if (selected_control) { + selected_control->connect(SceneStringName(draw), update_overlays); + } + + plugin->update_overlays(); +} + +void ControlOffsetTransformPreview::forward_canvas_draw_over_viewport(Control *p_overlay) const { + Control *selected_control = ObjectDB::get_instance(selected_control_id); + if (!selected_control || !selected_control->is_offset_transform_enabled() || selected_control->is_offset_transform_visual_only()) { + return; + } + + Point2 top_left = Point2(); + Point2 bottom_right = selected_control->get_size(); + Point2 top_right = Point2(bottom_right.x, top_left.y); + Point2 bottom_left = Point2(top_left.x, bottom_right.y); + + Transform2D offset_transform = selected_control->get_offset_transform(); + // Skip drawing if the offset transform is singular (e.g., zero scale), as it cannot be inverted. + if (Math::abs(offset_transform.determinant()) < CMP_EPSILON) { + return; + } + + Transform2D control_transform_without_offset = selected_control->get_global_transform() * offset_transform.affine_inverse(); + top_left = control_transform_without_offset.xform(top_left); + bottom_right = control_transform_without_offset.xform(bottom_right); + top_right = control_transform_without_offset.xform(top_right); + bottom_left = control_transform_without_offset.xform(bottom_left); + + Transform2D canvas_transform = CanvasItemEditor::get_singleton()->get_canvas_transform(); + top_left = canvas_transform.xform(top_left); + bottom_right = canvas_transform.xform(bottom_right); + top_right = canvas_transform.xform(top_right); + bottom_left = canvas_transform.xform(bottom_left); + + Color color = Color(0.5, 0.5, 0.5, 0.75); + p_overlay->draw_dashed_line(top_left, top_right, color, 5.0, 4.0); + p_overlay->draw_dashed_line(top_right, bottom_right, color, 5.0, 4.0); + p_overlay->draw_dashed_line(bottom_right, bottom_left, color, 5.0, 4.0); + p_overlay->draw_dashed_line(bottom_left, top_left, color, 5.0, 4.0); +} + +ControlOffsetTransformPreview::ControlOffsetTransformPreview(EditorPlugin *p_plugin) { + plugin = p_plugin; +} + +void ControlEditorPlugin::edit(Object *p_object) { + offset_transform_preview->edit(Object::cast_to(p_object)); +} + +bool ControlEditorPlugin::handles(Object *p_object) const { + return Object::cast_to(p_object) != nullptr; +} + +void ControlEditorPlugin::forward_canvas_draw_over_viewport(Control *p_overlay) { + offset_transform_preview->forward_canvas_draw_over_viewport(p_overlay); +} + ControlEditorPlugin::ControlEditorPlugin() { toolbar = memnew(ControlEditorToolbar); toolbar->hide(); add_control_to_container(CONTAINER_CANVAS_EDITOR_MENU, toolbar); + offset_transform_preview = memnew(ControlOffsetTransformPreview(this)); + EditorNode::get_singleton()->get_gui_base()->add_child(offset_transform_preview); + Ref plugin; plugin.instantiate(); add_inspector_plugin(plugin); diff --git a/editor/scene/gui/control_editor_plugin.h b/editor/scene/gui/control_editor_plugin.h index b4c3b0e7915..3da59c6b612 100644 --- a/editor/scene/gui/control_editor_plugin.h +++ b/editor/scene/gui/control_editor_plugin.h @@ -252,14 +252,36 @@ class ControlEditorToolbar : public HBoxContainer { ControlEditorToolbar(); }; +class ControlOffsetTransformPreview : public Control { + GDCLASS(ControlOffsetTransformPreview, Control); + + EditorPlugin *plugin = nullptr; + ObjectID selected_control_id; ///< Invalid when no control is selected, or when it has been deleted. + + friend class ControlEditorPlugin; + +public: + void edit(Control *p_control); + + void forward_canvas_draw_over_viewport(Control *p_overlay) const; + + ControlOffsetTransformPreview(EditorPlugin *p_plugin); +}; + /// Editor plugin. class ControlEditorPlugin : public EditorPlugin { GDCLASS(ControlEditorPlugin, EditorPlugin); ControlEditorToolbar *toolbar = nullptr; + ControlOffsetTransformPreview *offset_transform_preview = nullptr; public: virtual String get_plugin_name() const override { return "Control"; } + virtual void edit(Object *p_object) override; + virtual bool handles(Object *p_object) const override; + + virtual void forward_canvas_draw_over_viewport(Control *p_overlay) override; + ControlEditorPlugin(); }; diff --git a/scene/gui/control.cpp b/scene/gui/control.cpp index 90805e0b51b..5e3d97d97a0 100644 --- a/scene/gui/control.cpp +++ b/scene/gui/control.cpp @@ -65,6 +65,7 @@ Dictionary Control::_edit_get_state() const { s["rotation"] = get_rotation(); s["scale"] = get_scale(); s["pivot"] = get_pivot_offset(); + s["pivot_ratio"] = get_pivot_offset_ratio(); Array anchors = { get_anchor(SIDE_LEFT), get_anchor(SIDE_TOP), get_anchor(SIDE_RIGHT), get_anchor(SIDE_BOTTOM) }; s["anchors"] = anchors; @@ -88,6 +89,7 @@ void Control::_edit_set_state(const Dictionary &p_state) { set_rotation(state["rotation"]); set_scale(state["scale"]); set_pivot_offset(state["pivot"]); + set_pivot_offset_ratio(state.get("pivot_ratio", Vector2())); Array anchors = state["anchors"]; @@ -155,14 +157,15 @@ bool Control::_edit_use_rotation() const { } void Control::_edit_set_pivot(const Point2 &p_pivot) { - Vector2 delta_pivot = p_pivot - get_pivot_offset(); + Vector2 delta_pivot = p_pivot - get_combined_pivot_offset(); Vector2 move = Vector2((std::cos(data.rotation) - 1.0) * delta_pivot.x - std::sin(data.rotation) * delta_pivot.y, std::sin(data.rotation) * delta_pivot.x + (std::cos(data.rotation) - 1.0) * delta_pivot.y); set_position(get_position() + move); set_pivot_offset(p_pivot); + set_pivot_offset_ratio(Vector2()); } Point2 Control::_edit_get_pivot() const { - return get_pivot_offset(); + return get_combined_pivot_offset(); } bool Control::_edit_use_pivot() const { @@ -525,6 +528,8 @@ void Control::_validate_property(PropertyInfo &p_property) const { } // Validate which positioning properties should be displayed depending on the parent and the layout mode. Control *parent_control = get_parent_control(); + bool is_anchor_offset_property_name = p_property.name.begins_with("offset_") && !p_property.name.begins_with("offset_transform_"); + if (Engine::get_singleton()->is_editor_hint() && !parent_control) { // If there is no parent control, display both anchor and container options. @@ -536,14 +541,14 @@ void Control::_validate_property(PropertyInfo &p_property) const { // Use the layout mode to display or hide advanced anchoring properties. bool use_custom_anchors = _get_anchors_layout_preset() == -1; // Custom "preset". - if (!use_custom_anchors && (p_property.name.begins_with("anchor_") || p_property.name.begins_with("offset_") || p_property.name.begins_with("grow_"))) { + if (!use_custom_anchors && (p_property.name.begins_with("anchor_") || is_anchor_offset_property_name || p_property.name.begins_with("grow_"))) { p_property.usage ^= PROPERTY_USAGE_EDITOR; } } else if (Object::cast_to(parent_control)) { // If the parent is a container, display only container-related properties. - if (p_property.name.begins_with("anchor_") || p_property.name.begins_with("offset_") || p_property.name.begins_with("grow_") || p_property.name == "anchors_preset") { + if (p_property.name.begins_with("anchor_") || is_anchor_offset_property_name || p_property.name.begins_with("grow_") || p_property.name == "anchors_preset") { p_property.usage ^= PROPERTY_USAGE_DEFAULT; - } else if (p_property.name == "position" || p_property.name == "rotation" || p_property.name == "scale" || p_property.name == "size" || p_property.name == "pivot_offset") { + } else if (p_property.name == "position" || p_property.name == "rotation" || p_property.name == "scale" || p_property.name == "size" || p_property.name == "pivot_offset" || p_property.name == "pivot_offset_ratio") { p_property.usage = PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_READ_ONLY; } else if (Engine::get_singleton()->is_editor_hint() && p_property.name == "layout_mode") { // Set the layout mode to be disabled with the proper value. @@ -607,7 +612,7 @@ void Control::_validate_property(PropertyInfo &p_property) const { p_property.usage ^= PROPERTY_USAGE_EDITOR; } bool use_custom_anchors = use_anchors && _get_anchors_layout_preset() == -1; // Custom "preset". - if (!use_custom_anchors && (p_property.name.begins_with("anchor_") || p_property.name.begins_with("offset_") || p_property.name.begins_with("grow_"))) { + if (!use_custom_anchors && (p_property.name.begins_with("anchor_") || is_anchor_offset_property_name || p_property.name.begins_with("grow_"))) { p_property.usage ^= PROPERTY_USAGE_EDITOR; } } @@ -716,8 +721,13 @@ Size2 Control::get_parent_area_size() const { Transform2D Control::_get_internal_transform() const { // T(pivot_offset) * R(rotation) * S(scale) * T(-pivot_offset) - Transform2D xform(data.rotation, data.scale, 0.0f, data.pivot_offset); - xform.translate_local(-data.pivot_offset); + Transform2D xform(data.rotation, data.scale, 0.0f, get_combined_pivot_offset()); + xform.translate_local(-get_combined_pivot_offset()); + + if (is_offset_transform_enabled() && !data.offset_transform->visual_only) { + xform *= get_offset_transform(); + } + return xform; } @@ -730,6 +740,10 @@ void Control::_update_canvas_item_transform() { xform[2] = (xform[2] + Vector2(0.5, 0.5)).floor(); } + if (is_offset_transform_enabled() && data.offset_transform->visual_only) { + xform *= get_offset_transform(); + } + RenderingServer::get_singleton()->canvas_item_set_transform(get_canvas_item(), xform); } @@ -1613,6 +1627,23 @@ real_t Control::get_rotation_degrees() const { return Math::rad_to_deg(get_rotation()); } +void Control::set_pivot_offset_ratio(const Vector2 &p_ratio) { + ERR_MAIN_THREAD_GUARD; + if (data.pivot_offset_ratio == p_ratio) { + return; + } + + data.pivot_offset_ratio = p_ratio; + queue_redraw(); + _notify_transform(); + queue_accessibility_update(); +} + +Vector2 Control::get_pivot_offset_ratio() const { + ERR_READ_THREAD_GUARD_V(Vector2()); + return data.pivot_offset_ratio; +} + void Control::set_pivot_offset(const Vector2 &p_pivot) { ERR_MAIN_THREAD_GUARD; if (data.pivot_offset == p_pivot) { @@ -1630,6 +1661,11 @@ Vector2 Control::get_pivot_offset() const { return data.pivot_offset; } +Vector2 Control::get_combined_pivot_offset() const { + ERR_READ_THREAD_GUARD_V(Vector2()); + return data.pivot_offset + data.pivot_offset_ratio * get_size(); +} + /// Sizes. void Control::_update_minimum_size() { @@ -1855,6 +1891,241 @@ real_t Control::get_stretch_ratio() const { return data.expand; } +// Offset transform. + +void Control::set_offset_transform_enabled(bool p_enabled) { + ERR_MAIN_THREAD_GUARD; + if (is_offset_transform_enabled() == p_enabled) { + return; + } + + if (p_enabled) { + _ensure_allocated_offset_transform(); + data.offset_transform->enabled = true; + } else { + // Never deallocate to not lose previously set values + data.offset_transform->enabled = false; + } + + queue_redraw(); + _notify_transform(); + queue_accessibility_update(); +} + +bool Control::is_offset_transform_enabled() const { + ERR_READ_THREAD_GUARD_V(false); + return data.offset_transform != nullptr && data.offset_transform->enabled; +} + +void Control::set_offset_transform_position(const Vector2 &p_offset) { + ERR_MAIN_THREAD_GUARD; + if (get_offset_transform_position() == p_offset) { + return; + } + + _ensure_allocated_offset_transform(); + data.offset_transform->translation_absolute = p_offset; + + if (!data.offset_transform->enabled) { + return; + } + + queue_redraw(); + _notify_transform(); + queue_accessibility_update(); +} + +Vector2 Control::get_offset_transform_position() const { + ERR_READ_THREAD_GUARD_V(Vector2()); + if (data.offset_transform == nullptr) { + return Data::OffsetTransform::DEFAULT_TRANSLATION_ABSOLUTE; + } + + return data.offset_transform->translation_absolute; +} + +void Control::set_offset_transform_position_ratio(const Vector2 &p_offset) { + ERR_MAIN_THREAD_GUARD; + if (get_offset_transform_position_ratio() == p_offset) { + return; + } + + _ensure_allocated_offset_transform(); + data.offset_transform->translation_relative = p_offset; + + if (!data.offset_transform->enabled) { + return; + } + + queue_redraw(); + _notify_transform(); + queue_accessibility_update(); +} + +Vector2 Control::get_offset_transform_position_ratio() const { + if (data.offset_transform == nullptr) { + return Data::OffsetTransform::DEFAULT_TRANSLATION_RELATIVE; + } + + return data.offset_transform->translation_relative; +} + +void Control::set_offset_transform_scale(const Vector2 &p_scale) { + ERR_MAIN_THREAD_GUARD; + if (get_offset_transform_scale() == p_scale) { + return; + } + + _ensure_allocated_offset_transform(); + data.offset_transform->scale = p_scale; + // Avoid having 0 scale values, can lead to errors in physics and rendering. + if (data.offset_transform->scale.x == 0) { + data.offset_transform->scale.x = CMP_EPSILON; + } + if (data.offset_transform->scale.y == 0) { + data.offset_transform->scale.y = CMP_EPSILON; + } + + if (!data.offset_transform->enabled) { + return; + } + + queue_redraw(); + _notify_transform(); + queue_accessibility_update(); +} + +Vector2 Control::get_offset_transform_scale() const { + ERR_READ_THREAD_GUARD_V(Data::OffsetTransform::DEFAULT_SCALE); + if (data.offset_transform == nullptr) { + return Data::OffsetTransform::DEFAULT_SCALE; + } + + return data.offset_transform->scale; +} + +void Control::set_offset_transform_rotation(real_t p_rotation) { + ERR_MAIN_THREAD_GUARD; + if (get_offset_transform_rotation() == p_rotation) { + return; + } + + _ensure_allocated_offset_transform(); + data.offset_transform->rotation = p_rotation; + + if (!data.offset_transform->enabled) { + return; + } + + queue_redraw(); + _notify_transform(); + queue_accessibility_update(); +} + +real_t Control::get_offset_transform_rotation() const { + ERR_READ_THREAD_GUARD_V(Data::OffsetTransform::DEFAULT_ROTATION); + if (data.offset_transform == nullptr) { + return Data::OffsetTransform::DEFAULT_ROTATION; + } + + return data.offset_transform->rotation; +} + +void Control::set_offset_transform_pivot(const Vector2 &p_pivot) { + ERR_MAIN_THREAD_GUARD; + if (get_offset_transform_pivot() == p_pivot) { + return; + } + + _ensure_allocated_offset_transform(); + data.offset_transform->pivot_absolute = p_pivot; + + if (!data.offset_transform->enabled) { + return; + } + + queue_redraw(); + _notify_transform(); + queue_accessibility_update(); +} + +Vector2 Control::get_offset_transform_pivot() const { + ERR_READ_THREAD_GUARD_V(Data::OffsetTransform::DEFAULT_PIVOT_ABSOLUTE); + if (data.offset_transform == nullptr) { + return Data::OffsetTransform::DEFAULT_PIVOT_ABSOLUTE; + } + + return data.offset_transform->pivot_absolute; +} + +void Control::set_offset_transform_pivot_ratio(const Vector2 &p_pivot) { + ERR_MAIN_THREAD_GUARD; + if (get_offset_transform_pivot_ratio() == p_pivot) { + return; + } + + _ensure_allocated_offset_transform(); + data.offset_transform->pivot_relative = p_pivot; + + if (!data.offset_transform->enabled) { + return; + } + + queue_redraw(); + _notify_transform(); + queue_accessibility_update(); +} + +Vector2 Control::get_offset_transform_pivot_ratio() const { + ERR_READ_THREAD_GUARD_V(Data::OffsetTransform::DEFAULT_PIVOT_RELATIVE); + if (data.offset_transform == nullptr) { + return Data::OffsetTransform::DEFAULT_PIVOT_RELATIVE; + } + + return data.offset_transform->pivot_relative; +} + +void Control::set_offset_transform_visual_only(bool p_enabled) { + ERR_MAIN_THREAD_GUARD; + if (is_offset_transform_visual_only() == p_enabled) { + return; + } + + _ensure_allocated_offset_transform(); + data.offset_transform->visual_only = p_enabled; + + if (!data.offset_transform->enabled) { + return; + } + + queue_redraw(); + _notify_transform(); + queue_accessibility_update(); +} + +bool Control::is_offset_transform_visual_only() const { + ERR_READ_THREAD_GUARD_V(false); + if (data.offset_transform == nullptr) { + return Data::OffsetTransform::DEFAULT_VISUAL_ONLY; + } + + return data.offset_transform->visual_only; +} + +Transform2D Control::get_offset_transform() const { + ERR_READ_THREAD_GUARD_V(Transform2D()); + if (!is_offset_transform_enabled()) { + return Transform2D(); + } + + Vector2 combined_translation = data.offset_transform->translation_absolute + data.offset_transform->translation_relative * get_size(); + Vector2 combined_pivot = data.offset_transform->pivot_absolute + data.offset_transform->pivot_relative * get_size(); + + Transform2D offset_xform(data.offset_transform->rotation, data.offset_transform->scale, 0.0f, combined_pivot + combined_translation); + offset_xform.translate_local(-combined_pivot); + return offset_xform; +} + // Input events. void Control::_call_gui_input(const Ref &p_event) { @@ -3669,6 +3940,14 @@ Control *Control::make_custom_tooltip(const String &p_text) const { return Object::cast_to(ret); } +void Control::_ensure_allocated_offset_transform() { + if (data.offset_transform != nullptr) { + return; + } + + data.offset_transform = memnew(Data::OffsetTransform); +} + // Base object overrides. void Control::_accessibility_action_foucs(const Variant &p_data) { @@ -4006,6 +4285,7 @@ void Control::_bind_methods() { ClassDB::bind_method(D_METHOD("set_rotation_degrees", "degrees"), &Control::set_rotation_degrees); ClassDB::bind_method(D_METHOD("set_scale", "scale"), &Control::set_scale); ClassDB::bind_method(D_METHOD("set_pivot_offset", "pivot_offset"), &Control::set_pivot_offset); + ClassDB::bind_method(D_METHOD("set_pivot_offset_ratio", "ratio"), &Control::set_pivot_offset_ratio); ClassDB::bind_method(D_METHOD("get_begin"), &Control::get_begin); ClassDB::bind_method(D_METHOD("get_end"), &Control::get_end); ClassDB::bind_method(D_METHOD("get_position"), &Control::get_position); @@ -4014,6 +4294,8 @@ void Control::_bind_methods() { ClassDB::bind_method(D_METHOD("get_rotation_degrees"), &Control::get_rotation_degrees); ClassDB::bind_method(D_METHOD("get_scale"), &Control::get_scale); ClassDB::bind_method(D_METHOD("get_pivot_offset"), &Control::get_pivot_offset); + ClassDB::bind_method(D_METHOD("get_pivot_offset_ratio"), &Control::get_pivot_offset_ratio); + ClassDB::bind_method(D_METHOD("get_combined_pivot_offset"), &Control::get_combined_pivot_offset); ClassDB::bind_method(D_METHOD("get_custom_minimum_size"), &Control::get_custom_minimum_size); ClassDB::bind_method(D_METHOD("get_parent_area_size"), &Control::get_parent_area_size); ClassDB::bind_method(D_METHOD("get_global_position"), &Control::get_global_position); @@ -4041,6 +4323,24 @@ void Control::_bind_methods() { ClassDB::bind_method(D_METHOD("set_v_size_flags", "flags"), &Control::set_v_size_flags); ClassDB::bind_method(D_METHOD("get_v_size_flags"), &Control::get_v_size_flags); + ClassDB::bind_method(D_METHOD("set_offset_transform_enabled", "enabled"), &Control::set_offset_transform_enabled); + ClassDB::bind_method(D_METHOD("is_offset_transform_enabled"), &Control::is_offset_transform_enabled); + ClassDB::bind_method(D_METHOD("set_offset_transform_position", "offset"), &Control::set_offset_transform_position); + ClassDB::bind_method(D_METHOD("get_offset_transform_position"), &Control::get_offset_transform_position); + ClassDB::bind_method(D_METHOD("set_offset_transform_position_ratio", "offset"), &Control::set_offset_transform_position_ratio); + ClassDB::bind_method(D_METHOD("get_offset_transform_position_ratio"), &Control::get_offset_transform_position_ratio); + ClassDB::bind_method(D_METHOD("set_offset_transform_scale", "scale"), &Control::set_offset_transform_scale); + ClassDB::bind_method(D_METHOD("get_offset_transform_scale"), &Control::get_offset_transform_scale); + ClassDB::bind_method(D_METHOD("set_offset_transform_rotation", "rotation"), &Control::set_offset_transform_rotation); + ClassDB::bind_method(D_METHOD("get_offset_transform_rotation"), &Control::get_offset_transform_rotation); + ClassDB::bind_method(D_METHOD("set_offset_transform_pivot", "pivot"), &Control::set_offset_transform_pivot); + ClassDB::bind_method(D_METHOD("get_offset_transform_pivot"), &Control::get_offset_transform_pivot); + ClassDB::bind_method(D_METHOD("set_offset_transform_pivot_ratio", "pivot"), &Control::set_offset_transform_pivot_ratio); + ClassDB::bind_method(D_METHOD("get_offset_transform_pivot_ratio"), &Control::get_offset_transform_pivot_ratio); + ClassDB::bind_method(D_METHOD("set_offset_transform_visual_only", "enabled"), &Control::set_offset_transform_visual_only); + ClassDB::bind_method(D_METHOD("is_offset_transform_visual_only"), &Control::is_offset_transform_visual_only); + ClassDB::bind_method(D_METHOD("get_offset_transform"), &Control::get_offset_transform); + ClassDB::bind_method(D_METHOD("set_theme", "theme"), &Control::set_theme); ClassDB::bind_method(D_METHOD("get_theme"), &Control::get_theme); @@ -4241,12 +4541,23 @@ void Control::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "rotation_degrees", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_rotation_degrees", "get_rotation_degrees"); ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "scale"), "set_scale", "get_scale"); ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "pivot_offset", PROPERTY_HINT_NONE, "suffix:px"), "set_pivot_offset", "get_pivot_offset"); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "pivot_offset_ratio"), "set_pivot_offset_ratio", "get_pivot_offset_ratio"); ADD_SUBGROUP("Container Sizing", "size_flags_"); ADD_PROPERTY(PropertyInfo(Variant::INT, "size_flags_horizontal", PROPERTY_HINT_FLAGS, "Fill:1,Expand:2,Shrink Center:4,Shrink End:8"), "set_h_size_flags", "get_h_size_flags"); ADD_PROPERTY(PropertyInfo(Variant::INT, "size_flags_vertical", PROPERTY_HINT_FLAGS, "Fill:1,Expand:2,Shrink Center:4,Shrink End:8"), "set_v_size_flags", "get_v_size_flags"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "size_flags_stretch_ratio", PROPERTY_HINT_RANGE, "0,20,0.01,or_greater"), "set_stretch_ratio", "get_stretch_ratio"); + ADD_GROUP("Offset Transform", "offset_transform_"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "offset_transform_enabled", PROPERTY_HINT_GROUP_ENABLE), "set_offset_transform_enabled", "is_offset_transform_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "offset_transform_position", PROPERTY_HINT_NONE, "suffix:px"), "set_offset_transform_position", "get_offset_transform_position"); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "offset_transform_position_ratio", PROPERTY_HINT_NONE), "set_offset_transform_position_ratio", "get_offset_transform_position_ratio"); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "offset_transform_scale", PROPERTY_HINT_LINK), "set_offset_transform_scale", "get_offset_transform_scale"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "offset_transform_rotation", PROPERTY_HINT_RANGE, "-360,360,0.1,or_less,or_greater,radians_as_degrees"), "set_offset_transform_rotation", "get_offset_transform_rotation"); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "offset_transform_pivot", PROPERTY_HINT_NONE, "suffix:px"), "set_offset_transform_pivot", "get_offset_transform_pivot"); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "offset_transform_pivot_ratio", PROPERTY_HINT_NONE), "set_offset_transform_pivot_ratio", "get_offset_transform_pivot_ratio"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "offset_transform_visual_only", PROPERTY_HINT_NONE), "set_offset_transform_visual_only", "is_offset_transform_visual_only"); + ADD_GROUP("Localization", ""); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "localize_numeral_system"), "set_localize_numeral_system", "is_localizing_numeral_system"); @@ -4423,6 +4734,10 @@ Control::Control() { Control::~Control() { memdelete(data.theme_owner); + if (data.offset_transform != nullptr) { + memdelete(data.offset_transform); + } + // Resources need to be disconnected. for (KeyValue> &E : data.theme_icon_override) { E.value->disconnect_changed(callable_mp(this, &Control::_notify_theme_override_changed)); diff --git a/scene/gui/control.h b/scene/gui/control.h index 71459d88b18..76cc09e83ef 100644 --- a/scene/gui/control.h +++ b/scene/gui/control.h @@ -190,6 +190,25 @@ class Control : public CanvasItem { /// This Data struct is to avoid namespace pollution in derived classes. struct Data { + struct OffsetTransform { + static constexpr Vector2 DEFAULT_TRANSLATION_ABSOLUTE = Vector2(); + static constexpr Vector2 DEFAULT_TRANSLATION_RELATIVE = Vector2(); + static constexpr Vector2 DEFAULT_SCALE = Vector2(1, 1); + static constexpr real_t DEFAULT_ROTATION = 0.0; + static constexpr Vector2 DEFAULT_PIVOT_ABSOLUTE = Vector2(); + static constexpr Vector2 DEFAULT_PIVOT_RELATIVE = Vector2(0.5, 0.5); + static constexpr bool DEFAULT_VISUAL_ONLY = true; + + bool enabled = false; + Vector2 translation_absolute = DEFAULT_TRANSLATION_ABSOLUTE; + Vector2 translation_relative = DEFAULT_TRANSLATION_RELATIVE; + Vector2 scale = DEFAULT_SCALE; + real_t rotation = DEFAULT_ROTATION; + Vector2 pivot_absolute = DEFAULT_PIVOT_ABSOLUTE; + Vector2 pivot_relative = DEFAULT_PIVOT_RELATIVE; + bool visual_only = DEFAULT_VISUAL_ONLY; + }; + bool initialized = false; /// @name Global Relations @@ -219,6 +238,9 @@ class Control : public CanvasItem { real_t rotation = 0.0; Vector2 scale = Vector2(1, 1); Vector2 pivot_offset; + Vector2 pivot_offset_ratio; + + OffsetTransform *offset_transform = nullptr; Point2 pos_cache; Size2 size_cache; @@ -371,6 +393,7 @@ class Control : public CanvasItem { /// @} /// @name Extra Properties /// @{ + void _ensure_allocated_offset_transform(); static int root_layout_direction; /// @} protected: @@ -538,8 +561,11 @@ class Control : public CanvasItem { void set_rotation_degrees(real_t p_degrees); real_t get_rotation() const; real_t get_rotation_degrees() const; + void set_pivot_offset_ratio(const Vector2 &p_ratio); + Vector2 get_pivot_offset_ratio() const; void set_pivot_offset(const Vector2 &p_pivot); Vector2 get_pivot_offset() const; + Vector2 get_combined_pivot_offset() const; void update_minimum_size(); @@ -560,6 +586,26 @@ class Control : public CanvasItem { void set_stretch_ratio(real_t p_ratio); real_t get_stretch_ratio() const; /// @} + /// @name Offset transform + /// @{ + void set_offset_transform_enabled(bool p_enabled); + bool is_offset_transform_enabled() const; + void set_offset_transform_position(const Vector2 &p_offset); + Vector2 get_offset_transform_position() const; + void set_offset_transform_position_ratio(const Vector2 &p_offset); + Vector2 get_offset_transform_position_ratio() const; + void set_offset_transform_scale(const Vector2 &p_scale); + Vector2 get_offset_transform_scale() const; + void set_offset_transform_rotation(real_t p_rotation); + real_t get_offset_transform_rotation() const; + void set_offset_transform_pivot(const Vector2 &p_pivot); + Vector2 get_offset_transform_pivot() const; + void set_offset_transform_pivot_ratio(const Vector2 &p_pivot); + Vector2 get_offset_transform_pivot_ratio() const; + void set_offset_transform_visual_only(bool p_enabled); + bool is_offset_transform_visual_only() const; + Transform2D get_offset_transform() const; + /// @} /// @name Input Events /// @{ virtual void gui_input(const Ref &p_event);