/**************************************************************************/ /* 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 &p_event) { ERR_FAIL_COND(p_event.is_null()); SplitContainer *sc = Object::cast_to(get_parent()); if (sc->collapsed || sc->valid_children.size() < 2u || !sc->dragging_enabled) { return; } Ref 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 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(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(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(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(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(get_parent()); touch_dragger->set_modulate(sc->theme_cache.touch_dragger_color); } } void SplitContainerDragger::_touch_dragger_gui_input(const Ref &p_event) { if (!touch_dragger) { return; } Ref mm = p_event; Ref mb = p_event; SplitContainer *sc = Object::cast_to(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(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(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(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(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(get_parent()); if (sc->theme_cache.autohide) { queue_redraw(); } } break; case NOTIFICATION_MOUSE_EXIT: { mouse_inside = false; SplitContainer *sc = Object::cast_to(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(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 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 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 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 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 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 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 SplitContainer::get_allowed_size_flags_horizontal() const { Vector 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 SplitContainer::get_allowed_size_flags_vertical() const { Vector 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 SplitContainer::get_drag_area_controls() { TypedArray 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); }