Browse Source

Add HScrollbar to ItemList

havi05 11 months ago
parent
commit
a9af65d368
3 changed files with 200 additions and 52 deletions
  1. 11 0
      doc/classes/ItemList.xml
  2. 180 50
      scene/gui/item_list.cpp
  3. 9 2
      scene/gui/item_list.h

+ 11 - 0
doc/classes/ItemList.xml

@@ -63,6 +63,13 @@
 				Forces an update to the list size based on its items. This happens automatically whenever size of the items, or other relevant settings like [member auto_height], change. The method can be used to trigger the update ahead of next drawing pass.
 				Forces an update to the list size based on its items. This happens automatically whenever size of the items, or other relevant settings like [member auto_height], change. The method can be used to trigger the update ahead of next drawing pass.
 			</description>
 			</description>
 		</method>
 		</method>
+		<method name="get_h_scroll_bar">
+			<return type="HScrollBar" />
+			<description>
+				Returns the horizontal scrollbar.
+				[b]Warning:[/b] This is a required internal node, removing and freeing it may cause a crash. If you wish to hide it or any of its children, use their [member CanvasItem.visible] property.
+			</description>
+		</method>
 		<method name="get_item_at_position" qualifiers="const">
 		<method name="get_item_at_position" qualifiers="const">
 			<return type="int" />
 			<return type="int" />
 			<param index="0" name="position" type="Vector2" />
 			<param index="0" name="position" type="Vector2" />
@@ -420,6 +427,10 @@
 		<member name="text_overrun_behavior" type="int" setter="set_text_overrun_behavior" getter="get_text_overrun_behavior" enum="TextServer.OverrunBehavior" default="3">
 		<member name="text_overrun_behavior" type="int" setter="set_text_overrun_behavior" getter="get_text_overrun_behavior" enum="TextServer.OverrunBehavior" default="3">
 			Sets the clipping behavior when the text exceeds an item's bounding rectangle. See [enum TextServer.OverrunBehavior] for a description of all modes.
 			Sets the clipping behavior when the text exceeds an item's bounding rectangle. See [enum TextServer.OverrunBehavior] for a description of all modes.
 		</member>
 		</member>
+		<member name="wraparound_items" type="bool" setter="set_wraparound_items" getter="has_wraparound_items" default="true">
+			If [code]true[/code], the control will automatically move items into a new row to fit its content. See also [HFlowContainer] for this behavior.
+			If [code]false[/code], the control will add a horizontal scrollbar to make all items visible.
+		</member>
 	</members>
 	</members>
 	<signals>
 	<signals>
 		<signal name="empty_clicked">
 		<signal name="empty_clicked">

+ 180 - 50
scene/gui/item_list.cpp

@@ -671,7 +671,9 @@ void ItemList::gui_input(const Ref<InputEvent> &p_event) {
 #define CAN_SELECT(i) (items[i].selectable && !items[i].disabled)
 #define CAN_SELECT(i) (items[i].selectable && !items[i].disabled)
 #define IS_SAME_ROW(i, row) (i / current_columns == row)
 #define IS_SAME_ROW(i, row) (i / current_columns == row)
 
 
-	double prev_scroll = scroll_bar->get_value();
+	double prev_scroll_v = scroll_bar_v->get_value();
+	double prev_scroll_h = scroll_bar_h->get_value();
+	bool scroll_value_modified = false;
 
 
 	Ref<InputEventMouseMotion> mm = p_event;
 	Ref<InputEventMouseMotion> mm = p_event;
 	if (defer_select_single >= 0 && mm.is_valid()) {
 	if (defer_select_single >= 0 && mm.is_valid()) {
@@ -785,11 +787,50 @@ void ItemList::gui_input(const Ref<InputEvent> &p_event) {
 			emit_signal(SNAME("empty_clicked"), get_local_mouse_position(), mb->get_button_index());
 			emit_signal(SNAME("empty_clicked"), get_local_mouse_position(), mb->get_button_index());
 		}
 		}
 	}
 	}
-	if (mb.is_valid() && mb->get_button_index() == MouseButton::WHEEL_UP && mb->is_pressed()) {
-		scroll_bar->set_value(scroll_bar->get_value() - scroll_bar->get_page() * mb->get_factor() / 8);
-	}
-	if (mb.is_valid() && mb->get_button_index() == MouseButton::WHEEL_DOWN && mb->is_pressed()) {
-		scroll_bar->set_value(scroll_bar->get_value() + scroll_bar->get_page() * mb->get_factor() / 8);
+	if (mb.is_valid()) { // Copied from ScrollContainer.
+		if (mb->is_pressed()) {
+			bool v_scroll_hidden = !scroll_bar_v->is_visible();
+			if (mb->get_button_index() == MouseButton::WHEEL_UP) {
+				// By default, the vertical orientation takes precedence. This is an exception.
+				if (mb->is_shift_pressed() || v_scroll_hidden) {
+					scroll_bar_h->scroll(-scroll_bar_h->get_page() / 8 * mb->get_factor());
+					scroll_value_modified = true;
+				} else {
+					scroll_bar_v->scroll(-scroll_bar_v->get_page() / 8 * mb->get_factor());
+					scroll_value_modified = true;
+				}
+			}
+			if (mb->get_button_index() == MouseButton::WHEEL_DOWN) {
+				if (mb->is_shift_pressed() || v_scroll_hidden) {
+					scroll_bar_h->scroll(scroll_bar_h->get_page() / 8 * mb->get_factor());
+					scroll_value_modified = true;
+				} else {
+					scroll_bar_v->scroll(scroll_bar_v->get_page() / 8 * mb->get_factor());
+					scroll_value_modified = true;
+				}
+			}
+
+			bool h_scroll_hidden = !scroll_bar_h->is_visible();
+			if (mb->get_button_index() == MouseButton::WHEEL_LEFT) {
+				// By default, the horizontal orientation takes precedence. This is an exception.
+				if (mb->is_shift_pressed() || h_scroll_hidden) {
+					scroll_bar_v->scroll(-scroll_bar_v->get_page() / 8 * mb->get_factor());
+					scroll_value_modified = true;
+				} else {
+					scroll_bar_h->scroll(-scroll_bar_h->get_page() / 8 * mb->get_factor());
+					scroll_value_modified = true;
+				}
+			}
+			if (mb->get_button_index() == MouseButton::WHEEL_RIGHT) {
+				if (mb->is_shift_pressed() || h_scroll_hidden) {
+					scroll_bar_v->scroll(scroll_bar_v->get_page() / 8 * mb->get_factor());
+					scroll_value_modified = true;
+				} else {
+					scroll_bar_h->scroll(scroll_bar_h->get_page() / 8 * mb->get_factor());
+					scroll_value_modified = true;
+				}
+			}
+		}
 	}
 	}
 
 
 	if (p_event->is_pressed() && items.size() > 0) {
 	if (p_event->is_pressed() && items.size() > 0) {
@@ -1002,10 +1043,11 @@ void ItemList::gui_input(const Ref<InputEvent> &p_event) {
 
 
 	Ref<InputEventPanGesture> pan_gesture = p_event;
 	Ref<InputEventPanGesture> pan_gesture = p_event;
 	if (pan_gesture.is_valid()) {
 	if (pan_gesture.is_valid()) {
-		scroll_bar->set_value(scroll_bar->get_value() + scroll_bar->get_page() * pan_gesture->get_delta().y / 8);
+		scroll_bar_v->set_value(scroll_bar_v->get_value() + scroll_bar_v->get_page() * pan_gesture->get_delta().y / 8);
+		scroll_bar_h->set_value(scroll_bar_h->get_value() + scroll_bar_h->get_page() * pan_gesture->get_delta().x / 8);
 	}
 	}
 
 
-	if (scroll_bar->get_value() != prev_scroll) {
+	if (scroll_value_modified && (scroll_bar_v->get_value() != prev_scroll_v || scroll_bar_h->get_value() != prev_scroll_h)) {
 		accept_event(); //accept event if scroll changed
 		accept_event(); //accept event if scroll changed
 	}
 	}
 
 
@@ -1061,16 +1103,26 @@ void ItemList::_notification(int p_what) {
 		case NOTIFICATION_DRAW: {
 		case NOTIFICATION_DRAW: {
 			force_update_list_size();
 			force_update_list_size();
 
 
-			int scroll_bar_minwidth = scroll_bar->get_minimum_size().x;
-			scroll_bar->set_anchor_and_offset(SIDE_LEFT, ANCHOR_END, -scroll_bar_minwidth);
-			scroll_bar->set_anchor_and_offset(SIDE_RIGHT, ANCHOR_END, 0);
-			scroll_bar->set_anchor_and_offset(SIDE_TOP, ANCHOR_BEGIN, theme_cache.panel_style->get_margin(SIDE_TOP));
-			scroll_bar->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, -theme_cache.panel_style->get_margin(SIDE_BOTTOM));
+			Size2 scroll_bar_h_min = scroll_bar_h->is_visible() ? scroll_bar_h->get_combined_minimum_size() : Size2();
+			Size2 scroll_bar_v_min = scroll_bar_v->is_visible() ? scroll_bar_v->get_combined_minimum_size() : Size2();
+
+			int left_margin = is_layout_rtl() ? theme_cache.panel_style->get_margin(SIDE_RIGHT) : theme_cache.panel_style->get_margin(SIDE_LEFT);
+			int right_margin = is_layout_rtl() ? theme_cache.panel_style->get_margin(SIDE_LEFT) : theme_cache.panel_style->get_margin(SIDE_RIGHT);
+
+			scroll_bar_v->set_anchor_and_offset(SIDE_LEFT, ANCHOR_END, -scroll_bar_v_min.width - right_margin);
+			scroll_bar_v->set_anchor_and_offset(SIDE_RIGHT, ANCHOR_END, -right_margin);
+			scroll_bar_v->set_anchor_and_offset(SIDE_TOP, ANCHOR_BEGIN, theme_cache.panel_style->get_margin(SIDE_TOP));
+			scroll_bar_v->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, -scroll_bar_h_min.height - theme_cache.panel_style->get_margin(SIDE_BOTTOM));
+
+			scroll_bar_h->set_anchor_and_offset(SIDE_LEFT, ANCHOR_BEGIN, left_margin);
+			scroll_bar_h->set_anchor_and_offset(SIDE_RIGHT, ANCHOR_END, -right_margin - scroll_bar_v_min.width);
+			scroll_bar_h->set_anchor_and_offset(SIDE_TOP, ANCHOR_END, -scroll_bar_h_min.height - theme_cache.panel_style->get_margin(SIDE_BOTTOM));
+			scroll_bar_h->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, -theme_cache.panel_style->get_margin(SIDE_BOTTOM));
 
 
 			Size2 size = get_size();
 			Size2 size = get_size();
 			int width = size.width - theme_cache.panel_style->get_minimum_size().width;
 			int width = size.width - theme_cache.panel_style->get_minimum_size().width;
-			if (scroll_bar->is_visible()) {
-				width -= scroll_bar_minwidth;
+			if (scroll_bar_v->is_visible()) {
+				width -= scroll_bar_v_min.width;
 			}
 			}
 
 
 			draw_style_box(theme_cache.panel_style, Rect2(Point2(), size));
 			draw_style_box(theme_cache.panel_style, Rect2(Point2(), size));
@@ -1090,22 +1142,38 @@ void ItemList::_notification(int p_what) {
 			// Ensure_selected_visible needs to be checked before we draw the list.
 			// Ensure_selected_visible needs to be checked before we draw the list.
 			if (ensure_selected_visible && current >= 0 && current < items.size()) {
 			if (ensure_selected_visible && current >= 0 && current < items.size()) {
 				Rect2 r = items[current].rect_cache;
 				Rect2 r = items[current].rect_cache;
-				int from = scroll_bar->get_value();
-				int to = from + scroll_bar->get_page();
+				int from_v = scroll_bar_v->get_value();
+				int to_v = from_v + scroll_bar_v->get_page();
+
+				if (r.position.y < from_v) {
+					scroll_bar_v->set_value(r.position.y);
+				} else if (r.position.y + r.size.y > to_v) {
+					scroll_bar_v->set_value(r.position.y + r.size.y - (to_v - from_v));
+				}
+				int from_h = scroll_bar_h->get_value();
+				int to_h = from_h + scroll_bar_h->get_page();
 
 
-				if (r.position.y < from) {
-					scroll_bar->set_value(r.position.y);
-				} else if (r.position.y + r.size.y > to) {
-					scroll_bar->set_value(r.position.y + r.size.y - (to - from));
+				if (r.position.x < from_h) {
+					scroll_bar_h->set_value(r.position.x);
+				} else if (r.position.x + r.size.x > to_h) {
+					scroll_bar_h->set_value(r.position.x + r.size.x - (to_h - from_h));
 				}
 				}
 			}
 			}
 
 
 			ensure_selected_visible = false;
 			ensure_selected_visible = false;
 
 
 			Vector2 base_ofs = theme_cache.panel_style->get_offset();
 			Vector2 base_ofs = theme_cache.panel_style->get_offset();
-			base_ofs.y -= int(scroll_bar->get_value());
+			base_ofs.y -= int(scroll_bar_v->get_value());
+			if (rtl) {
+				base_ofs.x += int(scroll_bar_h->get_value());
+			} else {
+				base_ofs.x -= int(scroll_bar_h->get_value());
+			}
 
 
 			// Define a visible frame to check against and optimize drawing.
 			// Define a visible frame to check against and optimize drawing.
+			if (!wraparound_items) {
+				size.width += (scroll_bar_h->get_max() - scroll_bar_h->get_page());
+			}
 			const Rect2 clip(-base_ofs, size);
 			const Rect2 clip(-base_ofs, size);
 
 
 			// Do a binary search to find the first separator that is below clip_position.y.
 			// Do a binary search to find the first separator that is below clip_position.y.
@@ -1132,7 +1200,11 @@ void ItemList::_notification(int p_what) {
 					}
 					}
 
 
 					const int y = base_ofs.y + separators[i];
 					const int y = base_ofs.y + separators[i];
-					draw_line(Vector2(theme_cache.panel_style->get_margin(SIDE_LEFT), y), Vector2(width, y), theme_cache.guide_color);
+					if (rtl && scroll_bar_v->is_visible()) {
+						draw_line(Vector2(theme_cache.panel_style->get_margin(SIDE_LEFT) * 2 + scroll_bar_v_min.width, y), Vector2(width + theme_cache.panel_style->get_margin(SIDE_LEFT) + scroll_bar_v_min.width, y), theme_cache.guide_color);
+					} else {
+						draw_line(Vector2(theme_cache.panel_style->get_margin(SIDE_LEFT), y), Vector2(width + theme_cache.panel_style->get_margin(SIDE_LEFT), y), theme_cache.guide_color);
+					}
 				}
 				}
 			}
 			}
 
 
@@ -1188,7 +1260,7 @@ void ItemList::_notification(int p_what) {
 					r.position += base_ofs;
 					r.position += base_ofs;
 
 
 					if (rtl) {
 					if (rtl) {
-						r.position.x = size.width - r.position.x - r.size.x;
+						r.position.x = size.width - r.position.x - r.size.x + theme_cache.panel_style->get_margin(SIDE_LEFT) - theme_cache.panel_style->get_margin(SIDE_RIGHT);
 					}
 					}
 
 
 					if (should_draw_selected_bg) {
 					if (should_draw_selected_bg) {
@@ -1351,11 +1423,11 @@ void ItemList::_notification(int p_what) {
 						text_ofs += base_ofs;
 						text_ofs += base_ofs;
 						text_ofs += items[i].rect_cache.position;
 						text_ofs += items[i].rect_cache.position;
 
 
-						float text_w = width - text_ofs.x;
+						float text_w = width - text_ofs.x + theme_cache.panel_style->get_margin(SIDE_LEFT);
 						items.write[i].text_buf->set_width(text_w);
 						items.write[i].text_buf->set_width(text_w);
 
 
 						if (rtl) {
 						if (rtl) {
-							text_ofs.x = size.width - width;
+							text_ofs.x = size.width - width - theme_cache.panel_style->get_margin(SIDE_RIGHT);
 							items.write[i].text_buf->set_alignment(HORIZONTAL_ALIGNMENT_RIGHT);
 							items.write[i].text_buf->set_alignment(HORIZONTAL_ALIGNMENT_RIGHT);
 						} else {
 						} else {
 							items.write[i].text_buf->set_alignment(HORIZONTAL_ALIGNMENT_LEFT);
 							items.write[i].text_buf->set_alignment(HORIZONTAL_ALIGNMENT_LEFT);
@@ -1365,8 +1437,14 @@ void ItemList::_notification(int p_what) {
 							items[i].text_buf->draw_outline(get_canvas_item(), text_ofs, theme_cache.font_outline_size, theme_cache.font_outline_color);
 							items[i].text_buf->draw_outline(get_canvas_item(), text_ofs, theme_cache.font_outline_size, theme_cache.font_outline_color);
 						}
 						}
 
 
-						if (width - text_ofs.x > 0) {
-							items[i].text_buf->draw(get_canvas_item(), text_ofs, txt_modulate);
+						if (rtl) {
+							if (width - items[i].rect_cache.position.x - (MAX(theme_cache.h_separation, 0) / 2) - int(scroll_bar_h->get_value()) > 0) {
+								items[i].text_buf->draw(get_canvas_item(), text_ofs, txt_modulate);
+							}
+						} else {
+							if (width - text_ofs.x + theme_cache.panel_style->get_margin(SIDE_LEFT) > 0) {
+								items[i].text_buf->draw(get_canvas_item(), text_ofs, txt_modulate);
+							}
 						}
 						}
 					}
 					}
 				}
 				}
@@ -1388,6 +1466,7 @@ void ItemList::_notification(int p_what) {
 
 
 			if (has_focus()) {
 			if (has_focus()) {
 				RenderingServer::get_singleton()->canvas_item_add_clip_ignore(get_canvas_item(), true);
 				RenderingServer::get_singleton()->canvas_item_add_clip_ignore(get_canvas_item(), true);
+				size.x -= (scroll_bar_h->get_max() - scroll_bar_h->get_page());
 				draw_style_box(theme_cache.focus_style, Rect2(Point2(), size));
 				draw_style_box(theme_cache.focus_style, Rect2(Point2(), size));
 				RenderingServer::get_singleton()->canvas_item_add_clip_ignore(get_canvas_item(), false);
 				RenderingServer::get_singleton()->canvas_item_add_clip_ignore(get_canvas_item(), false);
 			}
 			}
@@ -1400,7 +1479,7 @@ void ItemList::force_update_list_size() {
 		return;
 		return;
 	}
 	}
 
 
-	int scroll_bar_minwidth = scroll_bar->get_minimum_size().x;
+	int scroll_bar_v_minwidth = scroll_bar_v->get_minimum_size().x;
 	Size2 size = get_size();
 	Size2 size = get_size();
 	float max_column_width = 0.0;
 	float max_column_width = 0.0;
 
 
@@ -1461,6 +1540,9 @@ void ItemList::force_update_list_size() {
 	}
 	}
 
 
 	int fit_size = size.x - theme_cache.panel_style->get_minimum_size().width;
 	int fit_size = size.x - theme_cache.panel_style->get_minimum_size().width;
+	if (!wraparound_items) {
+		fit_size += (scroll_bar_h->get_max() - scroll_bar_h->get_page());
+	}
 
 
 	//2-attempt best fit
 	//2-attempt best fit
 	current_columns = 0x7FFFFFFF;
 	current_columns = 0x7FFFFFFF;
@@ -1479,7 +1561,7 @@ void ItemList::force_update_list_size() {
 		separators.clear();
 		separators.clear();
 
 
 		for (int i = 0; i < items.size(); i++) {
 		for (int i = 0; i < items.size(); i++) {
-			if (current_columns > 1 && items[i].rect_cache.size.width + ofs.x > fit_size && !auto_width) {
+			if (current_columns > 1 && items[i].rect_cache.size.width + ofs.x > fit_size && !auto_width && wraparound_items) {
 				// Went past.
 				// Went past.
 				current_columns = MAX(col, 1);
 				current_columns = MAX(col, 1);
 				all_fit = false;
 				all_fit = false;
@@ -1493,6 +1575,7 @@ void ItemList::force_update_list_size() {
 
 
 			max_h = MAX(max_h, items[i].rect_cache.size.y);
 			max_h = MAX(max_h, items[i].rect_cache.size.y);
 			ofs.x += items[i].rect_cache.size.x;
 			ofs.x += items[i].rect_cache.size.x;
+			max_w = MAX(max_w, ofs.x);
 
 
 			items.write[i].column = col;
 			items.write[i].column = col;
 			col++;
 			col++;
@@ -1505,7 +1588,6 @@ void ItemList::force_update_list_size() {
 					items.write[j].rect_cache.size.y = max_h;
 					items.write[j].rect_cache.size.y = max_h;
 				}
 				}
 
 
-				max_w = MAX(max_w, ofs.x);
 				ofs.x = 0;
 				ofs.x = 0;
 				ofs.y += max_h;
 				ofs.y += max_h;
 				col = 0;
 				col = 0;
@@ -1513,10 +1595,17 @@ void ItemList::force_update_list_size() {
 			}
 			}
 		}
 		}
 
 
-		float page = MAX(0, size.height - theme_cache.panel_style->get_minimum_size().height);
-		float max = MAX(page, ofs.y + max_h);
-		if (page >= max) {
-			fit_size -= scroll_bar_minwidth;
+		Size2 scroll_bar_v_min = scroll_bar_v->is_visible() ? scroll_bar_v->get_combined_minimum_size() : Size2();
+		float scroll_bar_v_page = MAX(0, size.height - theme_cache.panel_style->get_minimum_size().height);
+		float scroll_bar_v_max = MAX(scroll_bar_v_page, ofs.y + max_h);
+		float scroll_bar_h_page = MAX(0, size.width - theme_cache.panel_style->get_minimum_size().width);
+		float scroll_bar_h_max = 0;
+		if (!wraparound_items) {
+			scroll_bar_h_max = MAX(scroll_bar_h_page, max_w) - scroll_bar_v_min.width;
+		}
+
+		if (scroll_bar_v_page >= scroll_bar_v_max || is_layout_rtl()) {
+			fit_size -= scroll_bar_v_minwidth;
 		}
 		}
 
 
 		if (all_fit) {
 		if (all_fit) {
@@ -1530,19 +1619,36 @@ void ItemList::force_update_list_size() {
 			if (auto_width) {
 			if (auto_width) {
 				auto_width_value = max_w + theme_cache.panel_style->get_minimum_size().width;
 				auto_width_value = max_w + theme_cache.panel_style->get_minimum_size().width;
 			}
 			}
-			scroll_bar->set_max(max);
-			scroll_bar->set_page(page);
-			if (max <= page) {
-				scroll_bar->set_value(0);
-				scroll_bar->hide();
+			scroll_bar_v->set_max(scroll_bar_v_max);
+			scroll_bar_v->set_page(scroll_bar_v_page);
+			if (scroll_bar_v_max <= scroll_bar_v_page) {
+				scroll_bar_v->set_value(0);
+				scroll_bar_v->hide();
 			} else {
 			} else {
-				auto_width_value += scroll_bar_minwidth;
-				scroll_bar->show();
+				auto_width_value += scroll_bar_v_minwidth;
+				scroll_bar_h_max += scroll_bar_v_minwidth;
+				scroll_bar_v->show();
 
 
 				if (do_autoscroll_to_bottom) {
 				if (do_autoscroll_to_bottom) {
-					scroll_bar->set_value(max);
+					scroll_bar_v->set_value(scroll_bar_v_max);
 				}
 				}
 			}
 			}
+
+			if (is_layout_rtl()) {
+				scroll_bar_h->set_max(scroll_bar_h_page);
+				scroll_bar_h->set_min(-(scroll_bar_h_max - scroll_bar_h_page));
+			} else {
+				scroll_bar_h->set_max(scroll_bar_h_max);
+				scroll_bar_h->set_min(0);
+			}
+			scroll_bar_h->set_page(scroll_bar_h_page);
+			if (scroll_bar_h_max <= scroll_bar_h_page) {
+				scroll_bar_h->set_value(0);
+				scroll_bar_h->hide();
+			} else {
+				auto_height_value += scroll_bar_h->get_minimum_size().y;
+				scroll_bar_h->show();
+			}
 			break;
 			break;
 		}
 		}
 	}
 	}
@@ -1582,10 +1688,11 @@ String ItemList::_atr(int p_idx, const String &p_text) const {
 int ItemList::get_item_at_position(const Point2 &p_pos, bool p_exact) const {
 int ItemList::get_item_at_position(const Point2 &p_pos, bool p_exact) const {
 	Vector2 pos = p_pos;
 	Vector2 pos = p_pos;
 	pos -= theme_cache.panel_style->get_offset();
 	pos -= theme_cache.panel_style->get_offset();
-	pos.y += scroll_bar->get_value();
+	pos.y += scroll_bar_v->get_value();
+	pos.x += scroll_bar_h->get_value();
 
 
 	if (is_layout_rtl()) {
 	if (is_layout_rtl()) {
-		pos.x = get_size().width - pos.x;
+		pos.x = get_size().width - pos.x - theme_cache.panel_style->get_margin(SIDE_LEFT) - theme_cache.panel_style->get_margin(SIDE_RIGHT);
 	}
 	}
 
 
 	int closest = -1;
 	int closest = -1;
@@ -1594,7 +1701,7 @@ int ItemList::get_item_at_position(const Point2 &p_pos, bool p_exact) const {
 	for (int i = 0; i < items.size(); i++) {
 	for (int i = 0; i < items.size(); i++) {
 		Rect2 rc = items[i].rect_cache;
 		Rect2 rc = items[i].rect_cache;
 
 
-		if (i % current_columns == current_columns - 1) {
+		if (i % current_columns == current_columns - 1 && wraparound_items) {
 			rc.size.width = get_size().width - rc.position.x; // Make sure you can still select the last item when clicking past the column.
 			rc.size.width = get_size().width - rc.position.x; // Make sure you can still select the last item when clicking past the column.
 		}
 		}
 
 
@@ -1620,7 +1727,7 @@ bool ItemList::is_pos_at_end_of_items(const Point2 &p_pos) const {
 
 
 	Vector2 pos = p_pos;
 	Vector2 pos = p_pos;
 	pos -= theme_cache.panel_style->get_offset();
 	pos -= theme_cache.panel_style->get_offset();
-	pos.y += scroll_bar->get_value();
+	pos.y += scroll_bar_v->get_value();
 
 
 	if (is_layout_rtl()) {
 	if (is_layout_rtl()) {
 		pos.x = get_size().width - pos.x;
 		pos.x = get_size().width - pos.x;
@@ -1795,6 +1902,20 @@ TextServer::OverrunBehavior ItemList::get_text_overrun_behavior() const {
 	return text_overrun_behavior;
 	return text_overrun_behavior;
 }
 }
 
 
+void ItemList::set_wraparound_items(bool p_enable) {
+	if (wraparound_items == p_enable) {
+		return;
+	}
+
+	wraparound_items = p_enable;
+	shape_changed = true;
+	queue_redraw();
+}
+
+bool ItemList::has_wraparound_items() const {
+	return wraparound_items;
+}
+
 bool ItemList::_set(const StringName &p_name, const Variant &p_value) {
 bool ItemList::_set(const StringName &p_name, const Variant &p_value) {
 	if (property_helper.property_set_value(p_name, p_value)) {
 	if (property_helper.property_set_value(p_name, p_value)) {
 		return true;
 		return true;
@@ -1934,10 +2055,14 @@ void ItemList::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("ensure_current_is_visible"), &ItemList::ensure_current_is_visible);
 	ClassDB::bind_method(D_METHOD("ensure_current_is_visible"), &ItemList::ensure_current_is_visible);
 
 
 	ClassDB::bind_method(D_METHOD("get_v_scroll_bar"), &ItemList::get_v_scroll_bar);
 	ClassDB::bind_method(D_METHOD("get_v_scroll_bar"), &ItemList::get_v_scroll_bar);
+	ClassDB::bind_method(D_METHOD("get_h_scroll_bar"), &ItemList::get_h_scroll_bar);
 
 
 	ClassDB::bind_method(D_METHOD("set_text_overrun_behavior", "overrun_behavior"), &ItemList::set_text_overrun_behavior);
 	ClassDB::bind_method(D_METHOD("set_text_overrun_behavior", "overrun_behavior"), &ItemList::set_text_overrun_behavior);
 	ClassDB::bind_method(D_METHOD("get_text_overrun_behavior"), &ItemList::get_text_overrun_behavior);
 	ClassDB::bind_method(D_METHOD("get_text_overrun_behavior"), &ItemList::get_text_overrun_behavior);
 
 
+	ClassDB::bind_method(D_METHOD("set_wraparound_items", "enable"), &ItemList::set_wraparound_items);
+	ClassDB::bind_method(D_METHOD("has_wraparound_items"), &ItemList::has_wraparound_items);
+
 	ClassDB::bind_method(D_METHOD("force_update_list_size"), &ItemList::force_update_list_size);
 	ClassDB::bind_method(D_METHOD("force_update_list_size"), &ItemList::force_update_list_size);
 
 
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "select_mode", PROPERTY_HINT_ENUM, "Single,Multi,Toggle"), "set_select_mode", "get_select_mode");
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "select_mode", PROPERTY_HINT_ENUM, "Single,Multi,Toggle"), "set_select_mode", "get_select_mode");
@@ -1948,6 +2073,7 @@ void ItemList::_bind_methods() {
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "auto_width"), "set_auto_width", "has_auto_width");
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "auto_width"), "set_auto_width", "has_auto_width");
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "auto_height"), "set_auto_height", "has_auto_height");
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "auto_height"), "set_auto_height", "has_auto_height");
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "text_overrun_behavior", PROPERTY_HINT_ENUM, "Trim Nothing,Trim Characters,Trim Words,Ellipsis,Word Ellipsis"), "set_text_overrun_behavior", "get_text_overrun_behavior");
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "text_overrun_behavior", PROPERTY_HINT_ENUM, "Trim Nothing,Trim Characters,Trim Words,Ellipsis,Word Ellipsis"), "set_text_overrun_behavior", "get_text_overrun_behavior");
+	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "wraparound_items"), "set_wraparound_items", "has_wraparound_items");
 	ADD_ARRAY_COUNT("Items", "item_count", "set_item_count", "get_item_count", "item_");
 	ADD_ARRAY_COUNT("Items", "item_count", "set_item_count", "get_item_count", "item_");
 	ADD_GROUP("Columns", "");
 	ADD_GROUP("Columns", "");
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "max_columns", PROPERTY_HINT_RANGE, "0,10,1,or_greater"), "set_max_columns", "get_max_columns");
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "max_columns", PROPERTY_HINT_RANGE, "0,10,1,or_greater"), "set_max_columns", "get_max_columns");
@@ -2009,9 +2135,13 @@ void ItemList::_bind_methods() {
 }
 }
 
 
 ItemList::ItemList() {
 ItemList::ItemList() {
-	scroll_bar = memnew(VScrollBar);
-	add_child(scroll_bar, false, INTERNAL_MODE_FRONT);
-	scroll_bar->connect(SceneStringName(value_changed), callable_mp(this, &ItemList::_scroll_changed));
+	scroll_bar_v = memnew(VScrollBar);
+	add_child(scroll_bar_v, false, INTERNAL_MODE_FRONT);
+	scroll_bar_v->connect(SceneStringName(value_changed), callable_mp(this, &ItemList::_scroll_changed));
+
+	scroll_bar_h = memnew(HScrollBar);
+	add_child(scroll_bar_h, false, INTERNAL_MODE_FRONT);
+	scroll_bar_h->connect(SceneStringName(value_changed), callable_mp(this, &ItemList::_scroll_changed));
 
 
 	connect(SceneStringName(mouse_exited), callable_mp(this, &ItemList::_mouse_exited));
 	connect(SceneStringName(mouse_exited), callable_mp(this, &ItemList::_mouse_exited));
 
 

+ 9 - 2
scene/gui/item_list.h

@@ -107,12 +107,15 @@ private:
 	bool auto_height = false;
 	bool auto_height = false;
 	float auto_height_value = 0.0;
 	float auto_height_value = 0.0;
 
 
+	bool wraparound_items = true;
+
 	Vector<Item> items;
 	Vector<Item> items;
 	Vector<int> separators;
 	Vector<int> separators;
 
 
 	SelectMode select_mode = SELECT_SINGLE;
 	SelectMode select_mode = SELECT_SINGLE;
 	IconMode icon_mode = ICON_MODE_LEFT;
 	IconMode icon_mode = ICON_MODE_LEFT;
-	VScrollBar *scroll_bar = nullptr;
+	VScrollBar *scroll_bar_v = nullptr;
+	HScrollBar *scroll_bar_h = nullptr;
 	TextServer::OverrunBehavior text_overrun_behavior = TextServer::OVERRUN_TRIM_ELLIPSIS;
 	TextServer::OverrunBehavior text_overrun_behavior = TextServer::OVERRUN_TRIM_ELLIPSIS;
 
 
 	uint64_t search_time_msec = 0;
 	uint64_t search_time_msec = 0;
@@ -304,13 +307,17 @@ public:
 	void set_auto_height(bool p_enable);
 	void set_auto_height(bool p_enable);
 	bool has_auto_height() const;
 	bool has_auto_height() const;
 
 
+	void set_wraparound_items(bool p_enable);
+	bool has_wraparound_items() const;
+
 	Size2 get_minimum_size() const override;
 	Size2 get_minimum_size() const override;
 
 
 	void set_autoscroll_to_bottom(const bool p_enable);
 	void set_autoscroll_to_bottom(const bool p_enable);
 
 
 	void force_update_list_size();
 	void force_update_list_size();
 
 
-	VScrollBar *get_v_scroll_bar() { return scroll_bar; }
+	VScrollBar *get_v_scroll_bar() { return scroll_bar_v; }
+	HScrollBar *get_h_scroll_bar() { return scroll_bar_h; }
 
 
 	ItemList();
 	ItemList();
 	~ItemList();
 	~ItemList();