1
0
mirror of https://github.com/godotengine/godot.git synced 2025-12-02 16:48:55 +00:00
Files
godot/scene/gui/split_container.cpp

1303 lines
44 KiB
C++

/**************************************************************************/
/* split_container.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "split_container.h"
#include "split_container.compat.inc"
#include "scene/gui/texture_rect.h"
#include "scene/main/viewport.h"
#include "scene/theme/theme_db.h"
void SplitContainerDragger::gui_input(const Ref<InputEvent> &p_event) {
ERR_FAIL_COND(p_event.is_null());
SplitContainer *sc = Object::cast_to<SplitContainer>(get_parent());
if (sc->collapsed || sc->valid_children.size() < 2u || !sc->dragging_enabled) {
return;
}
Ref<InputEventMouseButton> mb = p_event;
if (mb.is_valid()) {
if (mb->get_button_index() == MouseButton::LEFT) {
if (mb->is_pressed()) {
// To match the visual position, clamp on the first split.
sc->_update_dragger_positions(0);
dragging = true;
sc->emit_signal(SNAME("drag_started"));
start_drag_split_offset = sc->get_split_offset(dragger_index);
if (sc->vertical) {
drag_from = (int)get_transform().xform(mb->get_position()).y;
} else {
drag_from = (int)get_transform().xform(mb->get_position()).x;
}
} else {
dragging = false;
queue_redraw();
sc->emit_signal(SNAME("drag_ended"));
}
}
}
Ref<InputEventMouseMotion> mm = p_event;
if (mm.is_valid()) {
if (!dragging) {
return;
}
Vector2i in_parent_pos = get_transform().xform(mm->get_position());
int new_drag_offset;
if (!sc->vertical && is_layout_rtl()) {
new_drag_offset = start_drag_split_offset - (in_parent_pos.x - drag_from);
} else {
new_drag_offset = start_drag_split_offset + ((sc->vertical ? in_parent_pos.y : in_parent_pos.x) - drag_from);
}
sc->set_split_offset(new_drag_offset, dragger_index);
sc->_update_dragger_positions(dragger_index);
sc->queue_sort();
sc->emit_signal(SNAME("dragged"), sc->get_split_offset(dragger_index));
}
}
Control::CursorShape SplitContainerDragger::get_cursor_shape(const Point2 &p_pos) const {
SplitContainer *sc = Object::cast_to<SplitContainer>(get_parent());
if (!sc->collapsed && sc->dragging_enabled) {
return (sc->vertical ? CURSOR_VSPLIT : CURSOR_HSPLIT);
}
return Control::get_cursor_shape(p_pos);
}
void SplitContainerDragger::_accessibility_action_inc(const Variant &p_data) {
SplitContainer *sc = Object::cast_to<SplitContainer>(get_parent());
if (sc->collapsed || sc->valid_children.size() < 2u || !sc->dragging_enabled) {
return;
}
sc->set_split_offset(sc->get_split_offset(dragger_index) - 10, dragger_index);
sc->clamp_split_offset(dragger_index);
}
void SplitContainerDragger::_accessibility_action_dec(const Variant &p_data) {
SplitContainer *sc = Object::cast_to<SplitContainer>(get_parent());
if (sc->collapsed || sc->valid_children.size() < 2u || !sc->dragging_enabled) {
return;
}
sc->set_split_offset(sc->get_split_offset(dragger_index) + 10, dragger_index);
sc->clamp_split_offset(dragger_index);
}
void SplitContainerDragger::_accessibility_action_set_value(const Variant &p_data) {
SplitContainer *sc = Object::cast_to<SplitContainer>(get_parent());
if (sc->collapsed || sc->valid_children.size() < 2u || !sc->dragging_enabled) {
return;
}
sc->set_split_offset(p_data, dragger_index);
sc->clamp_split_offset(dragger_index);
}
void SplitContainerDragger::_touch_dragger_mouse_exited() {
if (!dragging) {
SplitContainer *sc = Object::cast_to<SplitContainer>(get_parent());
touch_dragger->set_modulate(sc->theme_cache.touch_dragger_color);
}
}
void SplitContainerDragger::_touch_dragger_gui_input(const Ref<InputEvent> &p_event) {
if (!touch_dragger) {
return;
}
Ref<InputEventMouseMotion> mm = p_event;
Ref<InputEventMouseButton> mb = p_event;
SplitContainer *sc = Object::cast_to<SplitContainer>(get_parent());
if (mb.is_valid() && mb->get_button_index() == MouseButton::LEFT) {
if (mb->is_pressed()) {
touch_dragger->set_modulate(sc->theme_cache.touch_dragger_pressed_color);
} else {
touch_dragger->set_modulate(sc->theme_cache.touch_dragger_color);
}
}
if (mm.is_valid() && !dragging) {
touch_dragger->set_modulate(sc->theme_cache.touch_dragger_hover_color);
}
}
void SplitContainerDragger::set_touch_dragger_enabled(bool p_enabled) {
if (p_enabled) {
touch_dragger = memnew(TextureRect);
update_touch_dragger();
SplitContainer *sc = Object::cast_to<SplitContainer>(get_parent());
touch_dragger->set_modulate(sc->theme_cache.touch_dragger_color);
touch_dragger->connect(SceneStringName(gui_input), callable_mp(this, &SplitContainerDragger::_touch_dragger_gui_input));
touch_dragger->connect(SceneStringName(mouse_exited), callable_mp(this, &SplitContainerDragger::_touch_dragger_mouse_exited));
add_child(touch_dragger, false, Node::INTERNAL_MODE_FRONT);
} else {
if (touch_dragger) {
touch_dragger->queue_free();
touch_dragger = nullptr;
}
}
queue_redraw();
}
void SplitContainerDragger::update_touch_dragger() {
if (!touch_dragger) {
return;
}
SplitContainer *sc = Object::cast_to<SplitContainer>(get_parent());
touch_dragger->set_texture(sc->_get_touch_dragger_icon());
touch_dragger->set_anchors_and_offsets_preset(Control::PRESET_CENTER);
touch_dragger->set_default_cursor_shape(sc->vertical ? CURSOR_VSPLIT : CURSOR_HSPLIT);
}
void SplitContainerDragger::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_ACCESSIBILITY_UPDATE: {
RID ae = get_accessibility_element();
ERR_FAIL_COND(ae.is_null());
DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_SPLITTER);
SplitContainer *sc = Object::cast_to<SplitContainer>(get_parent());
if (sc->collapsed || sc->valid_children.size() < 2u || !sc->dragging_enabled) {
return;
}
sc->clamp_split_offset(dragger_index);
DisplayServer::get_singleton()->accessibility_update_set_num_value(ae, sc->get_split_offset(dragger_index));
DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_DECREMENT, callable_mp(this, &SplitContainerDragger::_accessibility_action_dec));
DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_INCREMENT, callable_mp(this, &SplitContainerDragger::_accessibility_action_inc));
DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_SET_VALUE, callable_mp(this, &SplitContainerDragger::_accessibility_action_set_value));
} break;
case NOTIFICATION_THEME_CHANGED: {
if (touch_dragger) {
SplitContainer *sc = Object::cast_to<SplitContainer>(get_parent());
touch_dragger->set_modulate(sc->theme_cache.touch_dragger_color);
touch_dragger->set_texture(sc->_get_touch_dragger_icon());
}
} break;
case NOTIFICATION_MOUSE_ENTER: {
mouse_inside = true;
SplitContainer *sc = Object::cast_to<SplitContainer>(get_parent());
if (sc->theme_cache.autohide) {
queue_redraw();
}
} break;
case NOTIFICATION_MOUSE_EXIT: {
mouse_inside = false;
SplitContainer *sc = Object::cast_to<SplitContainer>(get_parent());
if (sc->theme_cache.autohide) {
queue_redraw();
}
} break;
case NOTIFICATION_FOCUS_EXIT: {
if (dragging) {
dragging = false;
queue_redraw();
}
} break;
case NOTIFICATION_VISIBILITY_CHANGED: {
if (dragging && !is_visible_in_tree()) {
dragging = false;
}
} break;
case NOTIFICATION_DRAW: {
SplitContainer *sc = Object::cast_to<SplitContainer>(get_parent());
draw_style_box(sc->theme_cache.split_bar_background, split_bar_rect);
if (sc->dragger_visibility == SplitContainer::DRAGGER_VISIBLE && (dragging || mouse_inside || !sc->theme_cache.autohide) && !sc->touch_dragger_enabled) {
Ref<Texture2D> tex = sc->_get_grabber_icon();
float available_size = sc->vertical ? (sc->get_size().x - tex->get_size().x) : (sc->get_size().y - tex->get_size().y);
if (available_size - sc->drag_area_margin_begin - sc->drag_area_margin_end > 0) { // Draw the grabber only if it fits.
draw_texture(tex, (split_bar_rect.get_position() + (split_bar_rect.get_size() - tex->get_size()) * 0.5));
}
}
if (sc->show_drag_area && Engine::get_singleton()->is_editor_hint()) {
draw_rect(Rect2(Vector2(0, 0), get_size()), sc->dragging_enabled ? Color(1, 1, 0, 0.3) : Color(1, 0, 0, 0.3));
}
} break;
}
}
SplitContainerDragger::SplitContainerDragger() {
set_focus_mode(FOCUS_ACCESSIBILITY);
}
Ref<Texture2D> SplitContainer::_get_grabber_icon() const {
if (is_fixed) {
return theme_cache.grabber_icon;
} else {
if (vertical) {
return theme_cache.grabber_icon_v;
} else {
return theme_cache.grabber_icon_h;
}
}
}
Ref<Texture2D> SplitContainer::_get_touch_dragger_icon() const {
if (is_fixed) {
return theme_cache.touch_dragger_icon;
} else {
if (vertical) {
return theme_cache.touch_dragger_icon_v;
} else {
return theme_cache.touch_dragger_icon_h;
}
}
}
int SplitContainer::_get_separation() const {
if (dragger_visibility == DRAGGER_HIDDEN_COLLAPSED) {
return 0;
}
if (touch_dragger_enabled) {
return theme_cache.separation;
}
// DRAGGER_VISIBLE or DRAGGER_HIDDEN.
Ref<Texture2D> g = _get_grabber_icon();
return MAX(theme_cache.separation, vertical ? g->get_height() : g->get_width());
}
Point2i SplitContainer::_get_valid_range(int p_dragger_index) const {
ERR_FAIL_INDEX_V(p_dragger_index, (int)dragger_positions.size(), Point2i());
const int axis = vertical ? 1 : 0;
const int sep = _get_separation();
// Sum the minimum sizes on the left and right sides of the dragger.
Point2i position_range = Point2i(0, (int)get_size()[axis]);
position_range.x += sep * p_dragger_index;
position_range.y -= sep * ((int)dragger_positions.size() - p_dragger_index);
for (int i = 0; i < (int)valid_children.size(); i++) {
Control *child = valid_children[i];
ERR_FAIL_NULL_V(child, Point2i());
if (i <= p_dragger_index) {
position_range.x += (int)child->get_combined_minimum_size()[axis];
} else if (i > p_dragger_index) {
position_range.y -= (int)child->get_combined_minimum_size()[axis];
}
}
return position_range;
}
PackedInt32Array SplitContainer::_get_desired_sizes() const {
ERR_FAIL_COND_V((int)default_dragger_positions.size() != split_offsets.size() || (int)valid_children.size() - 1 != split_offsets.size(), PackedInt32Array());
PackedInt32Array desired_sizes;
desired_sizes.resize_uninitialized((int)valid_children.size());
const int sep = _get_separation();
const int axis = vertical ? 1 : 0;
int desired_start_pos = 0;
for (int i = 0; i < (int)valid_children.size() - 1; i++) {
const int desired_end_pos = default_dragger_positions[i] + split_offsets[i];
desired_sizes.write[i] = desired_end_pos - desired_start_pos;
desired_start_pos = desired_end_pos + sep;
}
desired_sizes.write[(int)valid_children.size() - 1] = (int)get_size()[axis] - desired_start_pos;
return desired_sizes;
}
void SplitContainer::_set_desired_sizes(const PackedInt32Array &p_desired_sizes, int p_priority_index) {
const int sep = _get_separation();
const int axis = vertical ? 1 : 0;
const real_t size = get_size()[axis];
real_t total_desired_size = 0;
if (!p_desired_sizes.is_empty()) {
ERR_FAIL_COND((int)valid_children.size() != p_desired_sizes.size());
total_desired_size += sep * (p_desired_sizes.size() - 1);
}
struct StretchData {
real_t min_size = 0;
real_t stretch_ratio = 0.0;
real_t final_size = 0;
};
// First pass, determine the total stretch amount.
real_t stretch_total = 0;
LocalVector<StretchData> stretch_data;
for (int i = 0; i < (int)valid_children.size(); i++) {
Control *child = valid_children[i];
StretchData sdata;
sdata.min_size = child->get_combined_minimum_size()[axis];
sdata.final_size = MAX(sdata.min_size, p_desired_sizes.is_empty() ? 0 : p_desired_sizes[i]);
total_desired_size += sdata.final_size;
// Treat the priority child as not expanded, so it doesn't shrink with other expanded children.
if (i != p_priority_index && child->get_stretch_ratio() > 0 && (vertical ? child->get_v_size_flags() : child->get_h_size_flags()).has_flag(SIZE_EXPAND)) {
sdata.stretch_ratio = child->get_stretch_ratio();
stretch_total += sdata.stretch_ratio;
}
stretch_data.push_back(sdata);
}
real_t available_space = size - total_desired_size;
// Grow expanding children.
if (available_space > 0) {
const real_t grow_amount = available_space / stretch_total;
for (StretchData &sdata : stretch_data) {
if (sdata.stretch_ratio <= 0) {
continue;
}
const real_t prev_size = sdata.final_size;
sdata.final_size = prev_size + grow_amount * sdata.stretch_ratio;
const real_t size_diff = prev_size - sdata.final_size;
available_space += size_diff;
}
}
// Shrink expanding children.
while (available_space < 0) {
real_t shrinkable_stretch_ratio = 0.0;
real_t shrinkable_amount = 0.0;
for (const StretchData &sdata : stretch_data) {
if (sdata.stretch_ratio <= 0 || sdata.final_size <= sdata.min_size) {
continue;
}
shrinkable_stretch_ratio += sdata.stretch_ratio;
shrinkable_amount += sdata.final_size - sdata.min_size;
}
if (shrinkable_stretch_ratio == 0) {
break;
}
const real_t shrink_amount = MIN(-available_space, shrinkable_amount) / shrinkable_stretch_ratio;
if (Math::is_zero_approx(shrink_amount)) {
break;
}
for (StretchData &sdata : stretch_data) {
if (sdata.stretch_ratio <= 0 || sdata.final_size <= sdata.min_size) {
continue;
}
const real_t prev_size = sdata.final_size;
sdata.final_size = CLAMP(prev_size - shrink_amount * sdata.stretch_ratio, sdata.min_size, sdata.final_size);
const real_t size_diff = prev_size - sdata.final_size;
available_space += size_diff;
}
}
// Shrink non-expanding children.
while (available_space < 0) {
// Get largest and target sizes.
real_t largest_size = 0;
real_t target_size = 0;
int largest_count = 0;
for (const StretchData &sdata : stretch_data) {
if (sdata.final_size <= sdata.min_size) {
continue;
}
if (sdata.final_size > largest_size) {
target_size = largest_size;
largest_size = sdata.final_size;
largest_count = 1;
} else if (sdata.final_size == largest_size) {
largest_count++;
} else if (sdata.final_size < largest_size) {
target_size = MAX(sdata.final_size, target_size);
}
}
if (largest_size <= 0) {
break;
}
// Don't shrink smaller than needed.
target_size = MAX(target_size, available_space / largest_count);
target_size = MIN(target_size, largest_size + (available_space / largest_count));
for (StretchData &sdata : stretch_data) {
if (sdata.final_size <= sdata.min_size) {
continue;
}
// Shrink all largest elements.
if (sdata.final_size == largest_size) {
sdata.final_size = CLAMP(target_size, sdata.min_size, sdata.final_size);
const real_t size_diff = largest_size - sdata.final_size;
available_space += size_diff;
}
}
if (Math::is_zero_approx(available_space)) {
break;
}
}
ERR_FAIL_COND((int)default_dragger_positions.size() != (int)stretch_data.size() - 1);
// Update the split offsets to match the desired sizes.
split_offsets.resize(MAX(1, (int)default_dragger_positions.size()));
int pos = 0;
real_t error_accumulator = 0.0;
for (int i = 0; i < (int)default_dragger_positions.size(); i++) {
int final_size = (int)stretch_data[i].final_size;
if (final_size == stretch_data[i].final_size) {
error_accumulator += stretch_data[i].final_size - final_size;
if (error_accumulator > 1.0) {
error_accumulator -= 1.0;
final_size += 1;
}
}
pos += final_size;
split_offsets.write[i] = pos - default_dragger_positions[i];
pos += sep;
}
}
void SplitContainer::_update_default_dragger_positions() {
if (valid_children.size() <= 1u) {
default_dragger_positions.clear();
return;
}
default_dragger_positions.resize((int)valid_children.size() - 1);
const int sep = _get_separation();
const int axis = vertical ? 1 : 0;
const int size = (int)get_size()[axis];
struct StretchData {
int min_size = 0;
real_t stretch_ratio = 0.0;
int final_size = 0;
bool expand_flag = false;
bool will_stretch = false;
};
// First pass, determine the total stretch amount.
real_t stretchable_space = size - sep * ((int)valid_children.size() - 1);
real_t stretch_total = 0;
int expand_count = 0;
LocalVector<StretchData> stretch_data;
for (const Control *child : valid_children) {
StretchData sdata;
sdata.min_size = (int)child->get_combined_minimum_size()[axis];
sdata.final_size = sdata.min_size;
if ((vertical ? child->get_v_size_flags() : child->get_h_size_flags()).has_flag(SIZE_EXPAND) && child->get_stretch_ratio() > 0) {
sdata.stretch_ratio = child->get_stretch_ratio();
stretch_total += sdata.stretch_ratio;
sdata.expand_flag = true;
sdata.will_stretch = true;
expand_count++;
} else {
stretchable_space -= sdata.min_size;
}
stretch_data.push_back(sdata);
}
#ifndef DISABLE_DEPRECATED
if (expand_count == 2 && valid_children.size() == 2u) {
// Special case when there are 2 expanded children, ignore minimum sizes.
const real_t ratio = stretch_data[0].stretch_ratio / (stretch_data[0].stretch_ratio + stretch_data[1].stretch_ratio);
default_dragger_positions[0] = (int)(size * ratio - sep * 0.5);
return;
}
#endif // DISABLE_DEPRECATED
// Determine final sizes if stretching.
while (stretch_total > 0.0 && stretchable_space > 0.0) {
bool refit_successful = true;
// Keep track of accumulated error in pixels.
float error = 0.0;
for (StretchData &sdata : stretch_data) {
if (!sdata.will_stretch) {
continue;
}
// Check if it reaches its minimum size.
const float desired_stretch_size = sdata.stretch_ratio / stretch_total * stretchable_space;
error += desired_stretch_size - (int)desired_stretch_size;
if (desired_stretch_size < sdata.min_size) {
// Will not be stretched, remove and retry.
stretch_total -= sdata.stretch_ratio;
stretchable_space -= sdata.min_size;
sdata.will_stretch = false;
sdata.final_size = sdata.min_size;
refit_successful = false;
break;
} else {
sdata.final_size = (int)desired_stretch_size;
// Dump accumulated error if one pixel or more.
if (error >= 1.0) {
sdata.final_size += 1;
error -= 1;
}
}
}
if (refit_successful) {
break;
}
}
// Set the default positions.
int pos = 0;
int expands_seen = 0;
for (int i = 0; i < (int)default_dragger_positions.size(); i++) {
pos += stretch_data[i].final_size;
if (stretch_data[i].expand_flag) {
expands_seen += 1;
}
if (expands_seen == 0) {
// Before all expand flags.
default_dragger_positions[i] = 0;
} else if (expands_seen >= expand_count) {
// After all expand flags.
default_dragger_positions[i] = size - sep;
} else {
default_dragger_positions[i] = pos;
}
pos += sep;
}
}
void SplitContainer::_update_dragger_positions(int p_clamp_index) {
if (p_clamp_index != -1) {
ERR_FAIL_INDEX(p_clamp_index, (int)dragger_positions.size());
}
const int sep = _get_separation();
const int axis = vertical ? 1 : 0;
const int size = (int)get_size()[axis];
dragger_positions.resize(default_dragger_positions.size());
if (split_offsets.size() < (int)default_dragger_positions.size() || split_offsets.is_empty()) {
split_offsets.resize_initialized(MAX(1, (int)default_dragger_positions.size()));
}
if (collapsed) {
for (int i = 0; i < (int)dragger_positions.size(); i++) {
dragger_positions[i] = default_dragger_positions[i];
const Point2i valid_range = _get_valid_range(i);
dragger_positions[i] = CLAMP(dragger_positions[i], valid_range.x, valid_range.y);
if (p_clamp_index != -1) {
split_offsets.write[i] = dragger_positions[i] - default_dragger_positions[i];
}
if (!vertical && is_layout_rtl()) {
dragger_positions[i] = size - dragger_positions[i] - sep;
}
}
return;
}
// Use split_offsets to find the desired dragger positions.
for (int i = 0; i < (int)dragger_positions.size(); i++) {
// Clamp the desired position to acceptable values.
const Point2i valid_range = _get_valid_range(i);
dragger_positions[i] = CLAMP(default_dragger_positions[i] + split_offsets[i], valid_range.x, valid_range.y);
}
// Prevent overlaps.
if (p_clamp_index == -1) {
// Check each dragger with the one to the right of it.
for (int i = 0; i < (int)dragger_positions.size() - 1; i++) {
const int check_min_size = (int)valid_children[i + 1]->get_combined_minimum_size()[axis];
const int push_pos = dragger_positions[i] + sep + check_min_size;
if (dragger_positions[i + 1] < push_pos) {
dragger_positions[i + 1] = push_pos;
const Point2i valid_range = _get_valid_range(i);
dragger_positions[i] = CLAMP(dragger_positions[i], valid_range.x, valid_range.y);
}
}
} else {
// Prioritize the active dragger.
const int dragging_position = dragger_positions[p_clamp_index];
// Push overlapping draggers to the left.
int accumulated_min_size = (int)valid_children[p_clamp_index]->get_combined_minimum_size()[axis];
for (int i = p_clamp_index - 1; i >= 0; i--) {
const int push_pos = dragging_position - sep * (p_clamp_index - i) - accumulated_min_size;
if (dragger_positions[i] > push_pos) {
dragger_positions[i] = push_pos;
}
accumulated_min_size += (int)valid_children[i]->get_combined_minimum_size()[axis];
}
// Push overlapping draggers to the right.
accumulated_min_size = 0;
for (int i = p_clamp_index + 1; i < (int)dragger_positions.size(); i++) {
accumulated_min_size += (int)valid_children[i]->get_combined_minimum_size()[axis];
const int push_pos = dragging_position + sep * (i - p_clamp_index) + accumulated_min_size;
if (dragger_positions[i] < push_pos) {
dragger_positions[i] = push_pos;
}
}
}
// Clamp the split_offset if requested.
if (p_clamp_index != -1) {
for (int i = 0; i < (int)dragger_positions.size(); i++) {
split_offsets.write[i] = dragger_positions[i] - default_dragger_positions[i];
}
}
// Invert if rtl.
if (!vertical && is_layout_rtl()) {
for (int i = 0; i < (int)dragger_positions.size(); i++) {
dragger_positions[i] = size - dragger_positions[i] - sep;
}
}
}
void SplitContainer::_resort() {
if (!is_visible_in_tree()) {
return;
}
if (valid_children.size() < 2u) {
if (valid_children.size() == 1u) {
// Only one valid child.
Control *child = valid_children[0];
fit_child_in_rect(child, Rect2(Point2(), get_size()));
}
for (SplitContainerDragger *dragger : dragging_area_controls) {
dragger->hide();
}
return;
}
for (SplitContainerDragger *dragger : dragging_area_controls) {
dragger->set_visible(!collapsed);
if (touch_dragger_enabled) {
dragger->touch_dragger->set_visible(dragging_enabled);
}
}
_update_default_dragger_positions();
_update_dragger_positions();
const int sep = _get_separation();
const int axis = vertical ? 1 : 0;
const Size2i new_size = get_size();
const bool rtl = is_layout_rtl();
// Move the children.
for (int i = 0; i < (int)valid_children.size(); i++) {
Control *child = valid_children[i];
int start_pos;
int end_pos;
if (!vertical && rtl) {
start_pos = i >= (int)dragger_positions.size() ? 0 : dragger_positions[i] + sep;
end_pos = i == 0 ? new_size[axis] : dragger_positions[i - 1];
} else {
start_pos = i == 0 ? 0 : dragger_positions[i - 1] + sep;
end_pos = i >= (int)dragger_positions.size() ? new_size[axis] : dragger_positions[i];
}
int size = end_pos - start_pos;
if (vertical) {
fit_child_in_rect(child, Rect2(Point2(0, start_pos), Size2(new_size.width, size)));
} else {
fit_child_in_rect(child, Rect2(Point2(start_pos, 0), Size2(size, new_size.height)));
}
}
_update_draggers();
// Update dragger positions.
const int dragger_ctrl_size = MAX(sep, theme_cache.minimum_grab_thickness);
const float split_bar_offset = (dragger_ctrl_size - sep) * 0.5;
ERR_FAIL_COND(dragging_area_controls.size() != dragger_positions.size());
for (int i = 0; i < (int)dragger_positions.size(); i++) {
dragging_area_controls[i]->set_mouse_filter(dragging_enabled ? MOUSE_FILTER_STOP : MOUSE_FILTER_IGNORE);
if (vertical) {
const Rect2 split_bar_rect = Rect2(rtl ? drag_area_margin_end : drag_area_margin_begin, dragger_positions[i], new_size.width - drag_area_margin_begin - drag_area_margin_end, sep);
dragging_area_controls[i]->set_rect(Rect2(split_bar_rect.position.x, split_bar_rect.position.y - split_bar_offset + drag_area_offset, split_bar_rect.size.x, dragger_ctrl_size));
dragging_area_controls[i]->split_bar_rect = Rect2(Vector2(0.0, int(split_bar_offset) - drag_area_offset), split_bar_rect.size);
} else {
const Rect2 split_bar_rect = Rect2(dragger_positions[i], drag_area_margin_begin, sep, new_size.height - drag_area_margin_begin - drag_area_margin_end);
dragging_area_controls[i]->set_rect(Rect2(split_bar_rect.position.x - split_bar_offset + drag_area_offset * (rtl ? -1 : 1), split_bar_rect.position.y, dragger_ctrl_size, split_bar_rect.size.y));
dragging_area_controls[i]->split_bar_rect = Rect2(Vector2(int(split_bar_offset) - drag_area_offset * (rtl ? -1 : 1), 0.0), split_bar_rect.size);
}
dragging_area_controls[i]->queue_redraw();
}
queue_redraw();
}
void SplitContainer::_update_draggers() {
const int valid_child_count = (int)valid_children.size();
const int dragger_count = valid_child_count - 1;
const int draggers_size_diff = dragger_count - (int)dragging_area_controls.size();
// Add new draggers.
for (int i = 0; i < draggers_size_diff; i++) {
SplitContainerDragger *dragger = memnew(SplitContainerDragger);
dragging_area_controls.push_back(dragger);
add_child(dragger, false, Node::INTERNAL_MODE_BACK);
if (touch_dragger_enabled) {
dragger->set_touch_dragger_enabled(true);
}
}
// Remove extra draggers.
for (int i = 0; i < -draggers_size_diff; i++) {
const int remove_at = (int)dragging_area_controls.size() - 1;
SplitContainerDragger *dragger = dragging_area_controls[remove_at];
dragging_area_controls.remove_at(remove_at);
remove_child(dragger);
memdelete(dragger);
}
// Make sure draggers have the correct index.
for (int i = 0; i < (int)dragging_area_controls.size(); i++) {
dragging_area_controls[i]->dragger_index = i;
}
}
Size2 SplitContainer::get_minimum_size() const {
const int sep = _get_separation();
const int axis = vertical ? 1 : 0;
const int other_axis = vertical ? 0 : 1;
Size2i minimum;
if (valid_children.size() >= 2u) {
minimum[axis] += sep * ((int)valid_children.size() - 1);
}
for (const Control *child : valid_children) {
const Size2 min_size = child->get_combined_minimum_size();
minimum[axis] += (int)min_size[axis];
minimum[other_axis] = (int)MAX(minimum[other_axis], min_size[other_axis]);
}
return minimum;
}
void SplitContainer::_validate_property(PropertyInfo &p_property) const {
if (is_fixed && p_property.name == "vertical") {
p_property.usage = PROPERTY_USAGE_NONE;
}
}
void SplitContainer::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_TRANSLATION_CHANGED:
case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: {
queue_sort();
} break;
case NOTIFICATION_POSTINITIALIZE: {
initialized = true;
} break;
case NOTIFICATION_SORT_CHILDREN: {
_resort();
} break;
case NOTIFICATION_THEME_CHANGED: {
update_minimum_size();
} break;
case NOTIFICATION_PREDELETE: {
valid_children.clear();
dragging_area_controls.clear();
} break;
}
}
void SplitContainer::add_child_notify(Node *p_child) {
Container::add_child_notify(p_child);
if (p_child->is_internal()) {
return;
}
Control *child = as_sortable_control(p_child, SortableVisibilityMode::IGNORE);
if (!child) {
return;
}
child->connect(SceneStringName(visibility_changed), callable_mp(this, &SplitContainer::_on_child_visibility_changed).bind(child));
if (child->is_visible()) {
_add_valid_child(child);
}
}
void SplitContainer::remove_child_notify(Node *p_child) {
Container::remove_child_notify(p_child);
if (p_child->is_internal()) {
return;
}
Control *child = as_sortable_control(p_child, SortableVisibilityMode::IGNORE);
if (!child) {
return;
}
child->disconnect(SceneStringName(visibility_changed), callable_mp(this, &SplitContainer::_on_child_visibility_changed));
if (child->is_visible()) {
_remove_valid_child(child);
}
}
void SplitContainer::move_child_notify(Node *p_child) {
Container::move_child_notify(p_child);
Control *moved_child = as_sortable_control(p_child, SortableVisibilityMode::IGNORE);
const int prev_index = valid_children.find(moved_child);
if (prev_index == -1) {
return;
}
PackedInt32Array desired_sizes;
if (initialized && !split_offset_pending && valid_children.size() > 2u && split_offsets.size() == (int)default_dragger_positions.size()) {
desired_sizes = _get_desired_sizes();
}
valid_children.remove_at(prev_index);
// Get new index.
int index = 0;
for (int i = 0; i < get_child_count(false); i++) {
Control *child = as_sortable_control(get_child(i, false), SortableVisibilityMode::IGNORE);
if (!child) {
continue;
}
if (child == moved_child) {
break;
}
if (valid_children.has(child)) {
index++;
}
}
valid_children.insert(index, moved_child);
if (desired_sizes.is_empty()) {
return;
}
const int prev_desired_size = desired_sizes[prev_index];
desired_sizes.remove_at(prev_index);
desired_sizes.insert(index, prev_desired_size);
_set_desired_sizes(desired_sizes, index);
}
void SplitContainer::_on_child_visibility_changed(Control *p_control) {
if (p_control->is_visible()) {
_add_valid_child(p_control);
} else {
_remove_valid_child(p_control);
}
}
void SplitContainer::_add_valid_child(Control *p_control) {
if (valid_children.has(p_control)) {
return;
}
// Get index to insert.
bool child_is_valid = false;
int index = 0;
for (int i = 0; i < get_child_count(false); i++) {
Control *child = as_sortable_control(get_child(i, false), SortableVisibilityMode::IGNORE);
if (!child) {
continue;
}
if (child == p_control) {
if (child->is_visible()) {
child_is_valid = true;
}
break;
}
if (valid_children.has(child)) {
index++;
}
}
if (!child_is_valid) {
return;
}
PackedInt32Array desired_sizes;
if (initialized && can_use_desired_sizes && !split_offset_pending && valid_children.size() >= 2u && split_offsets.size() == (int)default_dragger_positions.size()) {
desired_sizes = _get_desired_sizes();
}
valid_children.insert(index, p_control);
if (!initialized) {
// If not initialized, the theme cache isn't ready yet so return early.
return;
}
_update_default_dragger_positions();
queue_sort();
if (valid_children.size() <= 2u) {
// Already have first dragger.
return;
}
// Call deferred in case already adding or removing children.
callable_mp(this, &SplitContainer::_update_draggers).call_deferred();
if (split_offset_pending && split_offsets.size() == (int)valid_children.size() - 1) {
split_offset_pending = false;
}
if (desired_sizes.is_empty()) {
return;
}
// Use the child's existing size as it's desired size.
const int axis = vertical ? 1 : 0;
desired_sizes.insert(index, (int)p_control->get_size()[axis]);
_set_desired_sizes(desired_sizes, index);
}
void SplitContainer::_remove_valid_child(Control *p_control) {
const int index = valid_children.find(p_control);
if (index == -1) {
return;
}
PackedInt32Array desired_sizes;
if (initialized && !split_offset_pending && valid_children.size() > 2u && split_offsets.size() == (int)default_dragger_positions.size()) {
desired_sizes = _get_desired_sizes();
}
valid_children.remove_at(index);
if (!initialized) {
return;
}
// Only use desired sizes to change the split offset after the first time a child is removed.
// This allows adding children to not affect the split offsets when creating.
can_use_desired_sizes = valid_children.size() > 1u;
_update_default_dragger_positions();
queue_sort();
if (valid_children.size() <= 1u) {
// Don't remove last dragger.
return;
}
// Call deferred in case already adding or removing children.
callable_mp(this, &SplitContainer::_update_draggers).call_deferred();
if (split_offset_pending && split_offsets.size() == (int)valid_children.size() - 2) {
split_offset_pending = false;
}
if (desired_sizes.is_empty()) {
return;
}
desired_sizes.remove_at(index);
_set_desired_sizes(desired_sizes);
}
void SplitContainer::set_split_offset(int p_offset, int p_index) {
ERR_FAIL_INDEX(p_index, split_offsets.size());
if (split_offsets[p_index] == p_offset) {
return;
}
split_offsets.write[p_index] = p_offset;
queue_sort();
}
int SplitContainer::get_split_offset(int p_index) const {
ERR_FAIL_INDEX_V(p_index, split_offsets.size(), 0);
return split_offsets[p_index];
}
void SplitContainer::set_split_offsets(const PackedInt32Array &p_offsets) {
if (split_offsets == p_offsets) {
return;
}
split_offsets = p_offsets;
split_offset_pending = split_offsets.size() > 1 && (int)valid_children.size() - 1 != split_offsets.size();
queue_sort();
}
PackedInt32Array SplitContainer::get_split_offsets() const {
return split_offsets;
}
void SplitContainer::clamp_split_offset(int p_priority_index) {
ERR_FAIL_INDEX(p_priority_index, split_offsets.size());
if (valid_children.size() < 2u) {
// Needs at least two children.
return;
}
_update_dragger_positions(p_priority_index);
queue_sort();
}
void SplitContainer::set_collapsed(bool p_collapsed) {
if (collapsed == p_collapsed) {
return;
}
collapsed = p_collapsed;
queue_sort();
}
void SplitContainer::set_dragger_visibility(DraggerVisibility p_visibility) {
if (dragger_visibility == p_visibility) {
return;
}
dragger_visibility = p_visibility;
queue_sort();
}
SplitContainer::DraggerVisibility SplitContainer::get_dragger_visibility() const {
return dragger_visibility;
}
bool SplitContainer::is_collapsed() const {
return collapsed;
}
void SplitContainer::set_vertical(bool p_vertical) {
ERR_FAIL_COND_MSG(is_fixed, "Can't change orientation of " + get_class() + ".");
if (vertical == p_vertical) {
return;
}
vertical = p_vertical;
for (SplitContainerDragger *dragger : dragging_area_controls) {
dragger->update_touch_dragger();
}
update_minimum_size();
_resort();
}
bool SplitContainer::is_vertical() const {
return vertical;
}
void SplitContainer::set_dragging_enabled(bool p_enabled) {
if (dragging_enabled == p_enabled) {
return;
}
dragging_enabled = p_enabled;
if (!dragging_enabled) {
bool was_dragging = false;
for (SplitContainerDragger *dragger : dragging_area_controls) {
was_dragging |= dragger->dragging;
dragger->dragging = false;
}
if (was_dragging) {
emit_signal(SNAME("drag_ended"));
}
}
if (get_viewport()) {
get_viewport()->update_mouse_cursor_state();
}
_resort();
}
bool SplitContainer::is_dragging_enabled() const {
return dragging_enabled;
}
Vector<int> SplitContainer::get_allowed_size_flags_horizontal() const {
Vector<int> flags;
flags.append(SIZE_FILL);
if (!vertical) {
flags.append(SIZE_EXPAND);
}
flags.append(SIZE_SHRINK_BEGIN);
flags.append(SIZE_SHRINK_CENTER);
flags.append(SIZE_SHRINK_END);
return flags;
}
Vector<int> SplitContainer::get_allowed_size_flags_vertical() const {
Vector<int> flags;
flags.append(SIZE_FILL);
if (vertical) {
flags.append(SIZE_EXPAND);
}
flags.append(SIZE_SHRINK_BEGIN);
flags.append(SIZE_SHRINK_CENTER);
flags.append(SIZE_SHRINK_END);
return flags;
}
void SplitContainer::set_drag_area_margin_begin(int p_margin) {
if (drag_area_margin_begin == p_margin) {
return;
}
drag_area_margin_begin = p_margin;
queue_sort();
}
int SplitContainer::get_drag_area_margin_begin() const {
return drag_area_margin_begin;
}
void SplitContainer::set_drag_area_margin_end(int p_margin) {
if (drag_area_margin_end == p_margin) {
return;
}
drag_area_margin_end = p_margin;
queue_sort();
}
int SplitContainer::get_drag_area_margin_end() const {
return drag_area_margin_end;
}
void SplitContainer::set_drag_area_offset(int p_offset) {
if (drag_area_offset == p_offset) {
return;
}
drag_area_offset = p_offset;
queue_sort();
}
int SplitContainer::get_drag_area_offset() const {
return drag_area_offset;
}
void SplitContainer::set_show_drag_area_enabled(bool p_enabled) {
show_drag_area = p_enabled;
for (SplitContainerDragger *dragger : dragging_area_controls) {
dragger->queue_redraw();
}
}
bool SplitContainer::is_show_drag_area_enabled() const {
return show_drag_area;
}
TypedArray<Control> SplitContainer::get_drag_area_controls() {
TypedArray<Control> controls;
controls.resize((int)dragging_area_controls.size());
for (int i = 0; i < (int)dragging_area_controls.size(); i++) {
controls[i] = dragging_area_controls[i];
}
return controls;
}
void SplitContainer::set_touch_dragger_enabled(bool p_enabled) {
if (touch_dragger_enabled == p_enabled) {
return;
}
touch_dragger_enabled = p_enabled;
for (SplitContainerDragger *dragger : dragging_area_controls) {
dragger->set_touch_dragger_enabled(p_enabled);
}
}
bool SplitContainer::is_touch_dragger_enabled() const {
return touch_dragger_enabled;
}
void SplitContainer::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_split_offsets", "offsets"), &SplitContainer::set_split_offsets);
ClassDB::bind_method(D_METHOD("get_split_offsets"), &SplitContainer::get_split_offsets);
ClassDB::bind_method(D_METHOD("clamp_split_offset", "priority_index"), &SplitContainer::clamp_split_offset, DEFVAL(0));
ClassDB::bind_method(D_METHOD("set_collapsed", "collapsed"), &SplitContainer::set_collapsed);
ClassDB::bind_method(D_METHOD("is_collapsed"), &SplitContainer::is_collapsed);
ClassDB::bind_method(D_METHOD("set_dragger_visibility", "mode"), &SplitContainer::set_dragger_visibility);
ClassDB::bind_method(D_METHOD("get_dragger_visibility"), &SplitContainer::get_dragger_visibility);
ClassDB::bind_method(D_METHOD("set_vertical", "vertical"), &SplitContainer::set_vertical);
ClassDB::bind_method(D_METHOD("is_vertical"), &SplitContainer::is_vertical);
ClassDB::bind_method(D_METHOD("set_dragging_enabled", "dragging_enabled"), &SplitContainer::set_dragging_enabled);
ClassDB::bind_method(D_METHOD("is_dragging_enabled"), &SplitContainer::is_dragging_enabled);
ClassDB::bind_method(D_METHOD("set_drag_area_margin_begin", "margin"), &SplitContainer::set_drag_area_margin_begin);
ClassDB::bind_method(D_METHOD("get_drag_area_margin_begin"), &SplitContainer::get_drag_area_margin_begin);
ClassDB::bind_method(D_METHOD("set_drag_area_margin_end", "margin"), &SplitContainer::set_drag_area_margin_end);
ClassDB::bind_method(D_METHOD("get_drag_area_margin_end"), &SplitContainer::get_drag_area_margin_end);
ClassDB::bind_method(D_METHOD("set_drag_area_offset", "offset"), &SplitContainer::set_drag_area_offset);
ClassDB::bind_method(D_METHOD("get_drag_area_offset"), &SplitContainer::get_drag_area_offset);
ClassDB::bind_method(D_METHOD("set_drag_area_highlight_in_editor", "drag_area_highlight_in_editor"), &SplitContainer::set_show_drag_area_enabled);
ClassDB::bind_method(D_METHOD("is_drag_area_highlight_in_editor_enabled"), &SplitContainer::is_show_drag_area_enabled);
ClassDB::bind_method(D_METHOD("get_drag_area_controls"), &SplitContainer::get_drag_area_controls);
ClassDB::bind_method(D_METHOD("set_touch_dragger_enabled", "enabled"), &SplitContainer::set_touch_dragger_enabled);
ClassDB::bind_method(D_METHOD("is_touch_dragger_enabled"), &SplitContainer::is_touch_dragger_enabled);
ADD_SIGNAL(MethodInfo("dragged", PropertyInfo(Variant::INT, "offset")));
ADD_SIGNAL(MethodInfo("drag_started"));
ADD_SIGNAL(MethodInfo("drag_ended"));
ADD_PROPERTY(PropertyInfo(Variant::PACKED_INT32_ARRAY, "split_offsets", PROPERTY_HINT_NONE, "suffix:px"), "set_split_offsets", "get_split_offsets");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "collapsed"), "set_collapsed", "is_collapsed");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "dragging_enabled"), "set_dragging_enabled", "is_dragging_enabled");
ADD_PROPERTY(PropertyInfo(Variant::INT, "dragger_visibility", PROPERTY_HINT_ENUM, "Visible,Hidden,Hidden and Collapsed"), "set_dragger_visibility", "get_dragger_visibility");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "vertical"), "set_vertical", "is_vertical");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "touch_dragger_enabled"), "set_touch_dragger_enabled", "is_touch_dragger_enabled");
ADD_GROUP("Drag Area", "drag_area_");
ADD_PROPERTY(PropertyInfo(Variant::INT, "drag_area_margin_begin", PROPERTY_HINT_NONE, "suffix:px"), "set_drag_area_margin_begin", "get_drag_area_margin_begin");
ADD_PROPERTY(PropertyInfo(Variant::INT, "drag_area_margin_end", PROPERTY_HINT_NONE, "suffix:px"), "set_drag_area_margin_end", "get_drag_area_margin_end");
ADD_PROPERTY(PropertyInfo(Variant::INT, "drag_area_offset", PROPERTY_HINT_NONE, "suffix:px"), "set_drag_area_offset", "get_drag_area_offset");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "drag_area_highlight_in_editor"), "set_drag_area_highlight_in_editor", "is_drag_area_highlight_in_editor_enabled");
BIND_ENUM_CONSTANT(DRAGGER_VISIBLE);
BIND_ENUM_CONSTANT(DRAGGER_HIDDEN);
BIND_ENUM_CONSTANT(DRAGGER_HIDDEN_COLLAPSED);
BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, SplitContainer, touch_dragger_color);
BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, SplitContainer, touch_dragger_pressed_color);
BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, SplitContainer, touch_dragger_hover_color);
BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, SplitContainer, separation);
BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, SplitContainer, minimum_grab_thickness);
BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, SplitContainer, autohide);
BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, SplitContainer, touch_dragger_icon, "touch_dragger");
BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, SplitContainer, touch_dragger_icon_h, "h_touch_dragger");
BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, SplitContainer, touch_dragger_icon_v, "v_touch_dragger");
BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, SplitContainer, grabber_icon, "grabber");
BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, SplitContainer, grabber_icon_h, "h_grabber");
BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, SplitContainer, grabber_icon_v, "v_grabber");
BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, SplitContainer, split_bar_background, "split_bar_background");
#ifndef DISABLE_DEPRECATED
ClassDB::bind_method(D_METHOD("get_drag_area_control"), &SplitContainer::get_drag_area_control);
ClassDB::bind_method(D_METHOD("set_split_offset", "offset"), &SplitContainer::_set_split_offset_first);
ClassDB::bind_method(D_METHOD("get_split_offset"), &SplitContainer::_get_split_offset_first);
ADD_PROPERTY(PropertyInfo(Variant::INT, "split_offset", PROPERTY_HINT_NONE, String(), PROPERTY_USAGE_NO_EDITOR), "set_split_offset", "get_split_offset");
#endif // DISABLE_DEPRECATED
}
SplitContainer::SplitContainer(bool p_vertical) {
vertical = p_vertical;
split_offsets.push_back(0);
SplitContainerDragger *dragger = memnew(SplitContainerDragger);
dragging_area_controls.push_back(dragger);
add_child(dragger, false, Node::INTERNAL_MODE_BACK);
}