Browse Source

Merge pull request #75472 from YuriSizov/editor-iconography

Improve editor support for icons of custom, scripted, and GDExtension classes
Rémi Verschelde 2 years ago
parent
commit
21d080ead4

+ 9 - 0
core/extension/gdextension.cpp

@@ -549,6 +549,15 @@ Ref<Resource> GDExtensionResourceLoader::load(const String &p_path, const String
 		return Ref<Resource>();
 	}
 
+	// Handle icons if any are specified.
+	if (config->has_section("icons")) {
+		List<String> keys;
+		config->get_section_keys("icons", &keys);
+		for (const String &key : keys) {
+			lib->class_icon_paths[key] = config->get_value("icons", key);
+		}
+	}
+
 	return lib;
 }
 

+ 2 - 0
core/extension/gdextension.h

@@ -67,6 +67,8 @@ protected:
 	static void _bind_methods();
 
 public:
+	HashMap<String, String> class_icon_paths;
+
 	static String get_extension_list_config_file();
 	static String find_extension_library(const String &p_path, Ref<ConfigFile> p_config, std::function<bool(String)> p_has_feature, PackedStringArray *r_tags = nullptr);
 

+ 23 - 0
core/extension/gdextension_manager.cpp

@@ -50,6 +50,11 @@ GDExtensionManager::LoadStatus GDExtensionManager::load_extension(const String &
 			extension->initialize_library(GDExtension::InitializationLevel(i));
 		}
 	}
+
+	for (const KeyValue<String, String> &kv : extension->class_icon_paths) {
+		gdextension_class_icon_paths[kv.key] = kv.value;
+	}
+
 	gdextension_map[p_path] = extension;
 	return LOAD_STATUS_OK;
 }
@@ -74,6 +79,11 @@ GDExtensionManager::LoadStatus GDExtensionManager::unload_extension(const String
 			extension->deinitialize_library(GDExtension::InitializationLevel(i));
 		}
 	}
+
+	for (const KeyValue<String, String> &kv : extension->class_icon_paths) {
+		gdextension_class_icon_paths.erase(kv.key);
+	}
+
 	gdextension_map.erase(p_path);
 	return LOAD_STATUS_OK;
 }
@@ -95,6 +105,19 @@ Ref<GDExtension> GDExtensionManager::get_extension(const String &p_path) {
 	return E->value;
 }
 
+bool GDExtensionManager::class_has_icon_path(const String &p_class) const {
+	// TODO: Check that the icon belongs to a registered class somehow.
+	return gdextension_class_icon_paths.has(p_class);
+}
+
+String GDExtensionManager::class_get_icon_path(const String &p_class) const {
+	// TODO: Check that the icon belongs to a registered class somehow.
+	if (gdextension_class_icon_paths.has(p_class)) {
+		return gdextension_class_icon_paths[p_class];
+	}
+	return "";
+}
+
 void GDExtensionManager::initialize_extensions(GDExtension::InitializationLevel p_level) {
 	ERR_FAIL_COND(int32_t(p_level) - 1 != level);
 	for (KeyValue<String, Ref<GDExtension>> &E : gdextension_map) {

+ 4 - 0
core/extension/gdextension_manager.h

@@ -38,6 +38,7 @@ class GDExtensionManager : public Object {
 
 	int32_t level = -1;
 	HashMap<String, Ref<GDExtension>> gdextension_map;
+	HashMap<String, String> gdextension_class_icon_paths;
 
 	static void _bind_methods();
 
@@ -59,6 +60,9 @@ public:
 	Vector<String> get_loaded_extensions() const;
 	Ref<GDExtension> get_extension(const String &p_path);
 
+	bool class_has_icon_path(const String &p_class) const;
+	String class_get_icon_path(const String &p_class) const;
+
 	void initialize_extensions(GDExtension::InitializationLevel p_level);
 	void deinitialize_extensions(GDExtension::InitializationLevel p_level);
 

+ 4 - 1
doc/classes/Button.xml

@@ -48,7 +48,7 @@
 			When this property is enabled, text that is too large to fit the button is clipped, when disabled the Button will always be wide enough to hold the text.
 		</member>
 		<member name="expand_icon" type="bool" setter="set_expand_icon" getter="is_expand_icon" default="false">
-			When enabled, the button's icon will expand/shrink to fit the button's size while keeping its aspect.
+			When enabled, the button's icon will expand/shrink to fit the button's size while keeping its aspect. See also [theme_item icon_max_width].
 		</member>
 		<member name="flat" type="bool" setter="set_flat" getter="is_flat" default="false">
 			Flat buttons don't display decoration.
@@ -116,6 +116,9 @@
 		<theme_item name="h_separation" data_type="constant" type="int" default="2">
 			The horizontal space between [Button]'s icon and text. Negative values will be treated as [code]0[/code] when used.
 		</theme_item>
+		<theme_item name="icon_max_width" data_type="constant" type="int" default="0">
+			The maximum allowed width of the [Button]'s icon. This limit is applied on top of the default size of the icon, or its expanded size if [member expand_icon] is [code]true[/code]. The height is adjusted according to the icon's ratio.
+		</theme_item>
 		<theme_item name="outline_size" data_type="constant" type="int" default="0">
 			The size of the text outline.
 			[b]Note:[/b] If using a font with [member FontFile.multichannel_signed_distance_field] enabled, its [member FontFile.msdf_pixel_range] must be set to at least [i]twice[/i] the value of [theme_item outline_size] for outline rendering to look correct. Otherwise, the outline may appear to be cut off earlier than intended.

+ 18 - 0
doc/classes/PopupMenu.xml

@@ -202,6 +202,13 @@
 				Returns the icon of the item at the given [param index].
 			</description>
 		</method>
+		<method name="get_item_icon_max_width" qualifiers="const">
+			<return type="int" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the maximum allowed width of the icon for the item at the given [param index].
+			</description>
+		</method>
 		<method name="get_item_id" qualifiers="const">
 			<return type="int" />
 			<param index="0" name="index" type="int" />
@@ -397,6 +404,14 @@
 				Replaces the [Texture2D] icon of the item at the given [param index].
 			</description>
 		</method>
+		<method name="set_item_icon_max_width">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="width" type="int" />
+			<description>
+				Sets the maximum allowed width of the icon for the item at the given [param index]. This limit is applied on top of the default size of the icon and on top of [theme_item icon_max_width]. The height is adjusted according to the icon's ratio.
+			</description>
+		</method>
 		<method name="set_item_id">
 			<return type="void" />
 			<param index="0" name="index" type="int" />
@@ -573,6 +588,9 @@
 		<theme_item name="h_separation" data_type="constant" type="int" default="4">
 			The horizontal space between the item's elements.
 		</theme_item>
+		<theme_item name="icon_max_width" data_type="constant" type="int" default="0">
+			The maximum allowed width of the item's icon. This limit is applied on top of the default size of the icon, but before the value set with [method set_item_icon_max_width]. The height is adjusted according to the icon's ratio.
+		</theme_item>
 		<theme_item name="indent" data_type="constant" type="int" default="10">
 			Width of the single indentation level.
 		</theme_item>

+ 20 - 2
doc/classes/TabBar.xml

@@ -46,14 +46,21 @@
 			<return type="Texture2D" />
 			<param index="0" name="tab_idx" type="int" />
 			<description>
-				Returns the [Texture2D] for the right button of the tab at index [param tab_idx] or [code]null[/code] if the button has no [Texture2D].
+				Returns the icon for the right button of the tab at index [param tab_idx] or [code]null[/code] if the right button has no icon.
 			</description>
 		</method>
 		<method name="get_tab_icon" qualifiers="const">
 			<return type="Texture2D" />
 			<param index="0" name="tab_idx" type="int" />
 			<description>
-				Returns the [Texture2D] for the tab at index [param tab_idx] or [code]null[/code] if the tab has no [Texture2D].
+				Returns the icon for the tab at index [param tab_idx] or [code]null[/code] if the tab has no icon.
+			</description>
+		</method>
+		<method name="get_tab_icon_max_width" qualifiers="const">
+			<return type="int" />
+			<param index="0" name="tab_idx" type="int" />
+			<description>
+				Returns the maximum allowed width of the icon for the tab at index [param tab_idx].
 			</description>
 		</method>
 		<method name="get_tab_idx_at_point" qualifiers="const">
@@ -158,6 +165,14 @@
 				Sets an [param icon] for the tab at index [param tab_idx].
 			</description>
 		</method>
+		<method name="set_tab_icon_max_width">
+			<return type="void" />
+			<param index="0" name="tab_idx" type="int" />
+			<param index="1" name="width" type="int" />
+			<description>
+				Sets the maximum allowed width of the icon for the tab at index [param tab_idx]. This limit is applied on top of the default size of the icon and on top of [theme_item icon_max_width]. The height is adjusted according to the icon's ratio.
+			</description>
+		</method>
 		<method name="set_tab_language">
 			<return type="void" />
 			<param index="0" name="tab_idx" type="int" />
@@ -323,6 +338,9 @@
 		<theme_item name="h_separation" data_type="constant" type="int" default="4">
 			The horizontal separation between the elements inside tabs.
 		</theme_item>
+		<theme_item name="icon_max_width" data_type="constant" type="int" default="0">
+			The maximum allowed width of the tab's icon. This limit is applied on top of the default size of the icon, but before the value set with [method set_tab_icon_max_width]. The height is adjusted according to the icon's ratio.
+		</theme_item>
 		<theme_item name="outline_size" data_type="constant" type="int" default="0">
 			The size of the tab text outline.
 			[b]Note:[/b] If using a font with [member FontFile.multichannel_signed_distance_field] enabled, its [member FontFile.msdf_pixel_range] must be set to at least [i]twice[/i] the value of [theme_item outline_size] for outline rendering to look correct. Otherwise, the outline may appear to be cut off earlier than intended.

+ 3 - 0
doc/classes/TabContainer.xml

@@ -209,6 +209,9 @@
 		<theme_item name="font_unselected_color" data_type="color" type="Color" default="Color(0.7, 0.7, 0.7, 1)">
 			Font color of the other, unselected tabs.
 		</theme_item>
+		<theme_item name="icon_max_width" data_type="constant" type="int" default="0">
+			The maximum allowed width of the tab's icon. This limit is applied on top of the default size of the icon, but before the value set with [method TabBar.set_tab_icon_max_width]. The height is adjusted according to the icon's ratio.
+		</theme_item>
 		<theme_item name="icon_separation" data_type="constant" type="int" default="4">
 			Space between tab's name and its icon.
 		</theme_item>

+ 3 - 0
doc/classes/Tree.xml

@@ -518,6 +518,9 @@
 		<theme_item name="h_separation" data_type="constant" type="int" default="4">
 			The horizontal space between item cells. This is also used as the margin at the start of an item when folding is disabled.
 		</theme_item>
+		<theme_item name="icon_max_width" data_type="constant" type="int" default="0">
+			The maximum allowed width of the icon in item's cells. This limit is applied on top of the default size of the icon, but before the value set with [method TreeItem.set_icon_max_width]. The height is adjusted according to the icon's ratio.
+		</theme_item>
 		<theme_item name="item_margin" data_type="constant" type="int" default="16">
 			The horizontal margin at the start of an item. This is used when folding is enabled for the item.
 		</theme_item>

+ 2 - 2
doc/classes/TreeItem.xml

@@ -183,7 +183,7 @@
 			<return type="int" />
 			<param index="0" name="column" type="int" />
 			<description>
-				Returns the column's icon's maximum width.
+				Returns the maximum allowed width of the icon in the given [param column].
 			</description>
 		</method>
 		<method name="get_icon_modulate" qualifiers="const">
@@ -545,7 +545,7 @@
 			<param index="0" name="column" type="int" />
 			<param index="1" name="width" type="int" />
 			<description>
-				Sets the given column's icon's maximum width.
+				Sets the maximum allowed width of the icon in the given [param column]. This limit is applied on top of the default size of the icon and on top of [theme_item Tree.icon_max_width]. The height is adjusted according to the icon's ratio.
 			</description>
 		</method>
 		<method name="set_icon_modulate">

+ 5 - 0
editor/create_dialog.cpp

@@ -462,6 +462,11 @@ void CreateDialog::_notification(int p_what) {
 		} break;
 
 		case NOTIFICATION_THEME_CHANGED: {
+			const int icon_width = get_theme_constant(SNAME("class_icon_size"), SNAME("Editor"));
+			search_options->add_theme_constant_override("icon_max_width", icon_width);
+			favorites->add_theme_constant_override("icon_max_width", icon_width);
+			recent->set_fixed_icon_size(Size2(icon_width, icon_width));
+
 			_update_theme();
 		} break;
 	}

+ 63 - 2
editor/editor_data.cpp

@@ -31,10 +31,13 @@
 #include "editor_data.h"
 
 #include "core/config/project_settings.h"
+#include "core/extension/gdextension_manager.h"
 #include "core/io/file_access.h"
+#include "core/io/image_loader.h"
 #include "core/io/resource_loader.h"
 #include "editor/editor_node.h"
 #include "editor/editor_plugin.h"
+#include "editor/editor_scale.h"
 #include "editor/editor_undo_redo_manager.h"
 #include "editor/plugins/script_editor_plugin.h"
 #include "scene/resources/packed_scene.h"
@@ -457,10 +460,10 @@ void EditorData::add_custom_type(const String &p_type, const String &p_inherits,
 	ct.name = p_type;
 	ct.icon = p_icon;
 	ct.script = p_script;
+
 	if (!custom_types.has(p_inherits)) {
 		custom_types[p_inherits] = Vector<CustomType>();
 	}
-
 	custom_types[p_inherits].push_back(ct);
 }
 
@@ -1028,8 +1031,66 @@ void EditorData::script_class_load_icon_paths() {
 	}
 }
 
+Ref<Texture2D> EditorData::extension_class_get_icon(const String &p_class) const {
+	if (GDExtensionManager::get_singleton()->class_has_icon_path(p_class)) {
+		String icon_path = GDExtensionManager::get_singleton()->class_get_icon_path(p_class);
+		Ref<Texture2D> icon = _load_script_icon(icon_path);
+		if (icon.is_valid()) {
+			return icon;
+		}
+	}
+	return nullptr;
+}
+
+Ref<Texture2D> EditorData::_load_script_icon(const String &p_path) const {
+	if (!p_path.is_empty() && ResourceLoader::exists(p_path)) {
+		Ref<Texture2D> icon = ResourceLoader::load(p_path);
+		if (icon.is_valid()) {
+			return icon;
+		}
+	}
+	return nullptr;
+}
+
+Ref<Texture2D> EditorData::get_script_icon(const Ref<Script> &p_script) {
+	// Take from the local cache, if available.
+	if (_script_icon_cache.has(p_script) && _script_icon_cache[p_script].is_valid()) {
+		return _script_icon_cache[p_script];
+	}
+
+	Ref<Script> base_scr = p_script;
+	while (base_scr.is_valid()) {
+		// Check for scripted classes.
+		StringName class_name = script_class_get_name(base_scr->get_path());
+		String icon_path = script_class_get_icon_path(class_name);
+		Ref<Texture2D> icon = _load_script_icon(icon_path);
+		if (icon.is_valid()) {
+			_script_icon_cache[p_script] = icon;
+			return icon;
+		}
+
+		// Check for legacy custom classes defined by plugins.
+		// TODO: Should probably be deprecated in 4.x
+		const EditorData::CustomType *ctype = get_custom_type_by_path(base_scr->get_path());
+		if (ctype && ctype->icon.is_valid()) {
+			_script_icon_cache[p_script] = ctype->icon;
+			return ctype->icon;
+		}
+
+		// Move to the base class.
+		base_scr = base_scr->get_base_script();
+	}
+
+	// If no icon found, cache it as null.
+	_script_icon_cache[p_script] = Ref<Texture>();
+	return nullptr;
+}
+
+void EditorData::clear_script_icon_cache() {
+	_script_icon_cache.clear();
+}
+
 EditorData::EditorData() {
-	current_edited_scene = -1;
 	undo_redo_manager = memnew(EditorUndoRedoManager);
 	script_class_load_icon_paths();
 }

+ 8 - 0
editor/editor_data.h

@@ -144,6 +144,9 @@ private:
 
 	HashMap<StringName, String> _script_class_icon_paths;
 	HashMap<String, StringName> _script_class_file_to_path;
+	HashMap<Ref<Script>, Ref<Texture>> _script_icon_cache;
+
+	Ref<Texture2D> _load_script_icon(const String &p_path) const;
 
 public:
 	EditorPlugin *get_editor(Object *p_object);
@@ -240,6 +243,11 @@ public:
 	void script_class_save_icon_paths();
 	void script_class_load_icon_paths();
 
+	Ref<Texture2D> extension_class_get_icon(const String &p_class) const;
+
+	Ref<Texture2D> get_script_icon(const Ref<Script> &p_script);
+	void clear_script_icon_cache();
+
 	EditorData();
 	~EditorData();
 };

+ 5 - 13
editor/editor_help.cpp

@@ -263,16 +263,8 @@ void EditorHelp::_add_type(const String &p_type, const String &p_enum) {
 	class_desc->pop();
 }
 
-void EditorHelp::_add_type_icon(const String &p_type, int p_size) {
-	Ref<Texture2D> icon;
-	if (has_theme_icon(p_type, SNAME("EditorIcons"))) {
-		icon = get_theme_icon(p_type, SNAME("EditorIcons"));
-	} else if (ClassDB::class_exists(p_type) && ClassDB::is_parent_class(p_type, "Object")) {
-		icon = get_theme_icon(SNAME("Object"), SNAME("EditorIcons"));
-	} else {
-		icon = get_theme_icon(SNAME("ArrowRight"), SNAME("EditorIcons"));
-	}
-
+void EditorHelp::_add_type_icon(const String &p_type, int p_size, const String &p_fallback) {
+	Ref<Texture2D> icon = EditorNode::get_singleton()->get_class_icon(p_type, p_fallback);
 	Vector2i size = Vector2i(icon->get_width(), icon->get_height());
 	if (p_size > 0) {
 		// Ensures icon scales proportionally on both axis, based on icon height.
@@ -644,7 +636,7 @@ void EditorHelp::_update_doc() {
 	section_line.push_back(Pair<String, int>(TTR("Top"), 0));
 	_push_title_font();
 	class_desc->add_text(TTR("Class:") + " ");
-	_add_type_icon(edited_class, theme_cache.doc_title_font_size);
+	_add_type_icon(edited_class, theme_cache.doc_title_font_size, "Object");
 	class_desc->add_text(" ");
 	class_desc->push_color(theme_cache.headline_color);
 	_add_text(edited_class);
@@ -676,7 +668,7 @@ void EditorHelp::_update_doc() {
 		String inherits = cd.inherits;
 
 		while (!inherits.is_empty()) {
-			_add_type_icon(inherits);
+			_add_type_icon(inherits, theme_cache.doc_font_size, "ArrowRight");
 			class_desc->add_text(non_breaking_space); // Otherwise icon borrows hyperlink from _add_type().
 			_add_type(inherits);
 
@@ -709,7 +701,7 @@ void EditorHelp::_update_doc() {
 				if (prev) {
 					class_desc->add_text(" , ");
 				}
-				_add_type_icon(E.value.name);
+				_add_type_icon(E.value.name, theme_cache.doc_font_size, "ArrowRight");
 				class_desc->add_text(non_breaking_space); // Otherwise icon borrows hyperlink from _add_type().
 				_add_type(E.value.name);
 				prev = true;

+ 1 - 1
editor/editor_help.h

@@ -159,7 +159,7 @@ class EditorHelp : public VBoxContainer {
 
 	//void _button_pressed(int p_idx);
 	void _add_type(const String &p_type, const String &p_enum = String());
-	void _add_type_icon(const String &p_type, int p_size = 0);
+	void _add_type_icon(const String &p_type, int p_size = 0, const String &p_fallback = "");
 	void _add_method(const DocData::MethodDoc &p_method, bool p_overview = true);
 
 	void _add_bulletpoint();

+ 19 - 33
editor/editor_inspector.cpp

@@ -1134,17 +1134,21 @@ void EditorInspectorCategory::_notification(int p_what) {
 			int font_size = get_theme_font_size(SNAME("bold_size"), SNAME("EditorFonts"));
 
 			int hs = get_theme_constant(SNAME("h_separation"), SNAME("Tree"));
+			int icon_size = get_theme_constant(SNAME("class_icon_size"), SNAME("Editor"));
 
 			int w = font->get_string_size(label, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).width;
 			if (icon.is_valid()) {
-				w += hs + icon->get_width();
+				w += hs + icon_size;
 			}
 
 			int ofs = (get_size().width - w) / 2;
 
 			if (icon.is_valid()) {
-				draw_texture(icon, Point2(ofs, (get_size().height - icon->get_height()) / 2).floor());
-				ofs += hs + icon->get_width();
+				Size2 rect_size = Size2(icon_size, icon_size);
+				Point2 rect_pos = Point2(ofs, (get_size().height - icon_size) / 2).floor();
+				draw_texture_rect(icon, Rect2(rect_pos, rect_size));
+
+				ofs += hs + icon_size;
 			}
 
 			Color color = get_theme_color(SNAME("font_color"), SNAME("Tree"));
@@ -2753,46 +2757,28 @@ void EditorInspector::update_tree() {
 			String label = p.name;
 			doc_name = p.name;
 
-			// Set the category icon.
+			// Use category's owner script to update some of its information.
 			if (!EditorNode::get_editor_data().is_type_recognized(type) && p.hint_string.length() && FileAccess::exists(p.hint_string)) {
-				// If we have a category inside a script, search for the first script with a valid icon.
+				StringName script_name;
+
 				Ref<Script> scr = ResourceLoader::load(p.hint_string, "Script");
-				StringName base_type;
-				StringName name;
 				if (scr.is_valid()) {
-					base_type = scr->get_instance_base_type();
-					name = EditorNode::get_editor_data().script_class_get_name(scr->get_path());
+					script_name = EditorNode::get_editor_data().script_class_get_name(scr->get_path());
+
+					// Update the docs reference and the label based on the script.
 					Vector<DocData::ClassDoc> docs = scr->get_documentation();
 					if (!docs.is_empty()) {
 						doc_name = docs[0].name;
 					}
-					if (name != StringName() && label != name) {
-						label = name;
+					if (script_name != StringName() && label != script_name) {
+						label = script_name;
 					}
 				}
-				while (scr.is_valid()) {
-					name = EditorNode::get_editor_data().script_class_get_name(scr->get_path());
-					String icon_path = EditorNode::get_editor_data().script_class_get_icon_path(name);
-					if (name != StringName() && !icon_path.is_empty()) {
-						category->icon = ResourceLoader::load(icon_path, "Texture");
-						break;
-					}
 
-					const EditorData::CustomType *ctype = EditorNode::get_editor_data().get_custom_type_by_path(scr->get_path());
-					if (ctype) {
-						category->icon = ctype->icon;
-						break;
-					}
-					scr = scr->get_base_script();
-				}
-				if (category->icon.is_null() && has_theme_icon(base_type, SNAME("EditorIcons"))) {
-					category->icon = get_theme_icon(base_type, SNAME("EditorIcons"));
-				}
-			}
-			if (category->icon.is_null()) {
-				if (!type.is_empty()) { // Can happen for built-in scripts.
-					category->icon = EditorNode::get_singleton()->get_class_icon(type, "Object");
-				}
+				// Find the corresponding icon.
+				category->icon = EditorNode::get_singleton()->get_class_icon(script_name, "Object");
+			} else if (!type.is_empty()) {
+				category->icon = EditorNode::get_singleton()->get_class_icon(type, "Object");
 			}
 
 			// Set the category label.

+ 62 - 95
editor/editor_node.cpp

@@ -34,7 +34,6 @@
 #include "core/input/input.h"
 #include "core/io/config_file.h"
 #include "core/io/file_access.h"
-#include "core/io/image_loader.h"
 #include "core/io/resource_loader.h"
 #include "core/io/resource_saver.h"
 #include "core/object/class_db.h"
@@ -682,10 +681,6 @@ void EditorNode::_notification(int p_what) {
 			editor_data.clear_edited_scenes();
 		} break;
 
-		case Control::NOTIFICATION_THEME_CHANGED: {
-			scene_tab_add_ph->set_custom_minimum_size(scene_tab_add->get_minimum_size());
-		} break;
-
 		case NOTIFICATION_READY: {
 			{
 				_initializing_plugins = true;
@@ -773,6 +768,9 @@ void EditorNode::_notification(int p_what) {
 				bottom_panel->add_theme_style_override("panel", gui_base->get_theme_stylebox(SNAME("BottomPanel"), SNAME("EditorStyles")));
 				tabbar_panel->add_theme_style_override("panel", gui_base->get_theme_stylebox(SNAME("tabbar_background"), SNAME("TabContainer")));
 
+				scene_tabs->add_theme_constant_override("icon_max_width", gui_base->get_theme_constant(SNAME("class_icon_size"), SNAME("Editor")));
+				scene_tab_add_ph->set_custom_minimum_size(scene_tab_add->get_minimum_size());
+
 				main_menu->add_theme_style_override("hover", gui_base->get_theme_stylebox(SNAME("MenuHover"), SNAME("EditorStyles")));
 			}
 
@@ -3682,7 +3680,7 @@ void EditorNode::_remove_edited_scene(bool p_change_tab) {
 
 void EditorNode::_remove_scene(int index, bool p_change_tab) {
 	// Clear icon cache in case some scripts are no longer needed.
-	script_icon_cache.clear();
+	editor_data.clear_script_icon_cache();
 
 	if (editor_data.get_edited_scene() == index) {
 		// Scene to remove is current scene.
@@ -4446,18 +4444,6 @@ StringName EditorNode::get_object_custom_type_name(const Object *p_object) const
 	return StringName();
 }
 
-Ref<ImageTexture> EditorNode::_load_custom_class_icon(const String &p_path) const {
-	if (p_path.length()) {
-		Ref<Image> img = memnew(Image);
-		Error err = ImageLoader::load_image(p_path, img);
-		if (err == OK) {
-			img->resize(16 * EDSCALE, 16 * EDSCALE, Image::INTERPOLATE_LANCZOS);
-			return ImageTexture::create_from_image(img);
-		}
-	}
-	return nullptr;
-}
-
 void EditorNode::_pick_main_scene_custom_action(const String &p_custom_action_name) {
 	if (p_custom_action_name == "select_current") {
 		Node *scene = editor_data.get_edited_scene_root();
@@ -4480,106 +4466,86 @@ void EditorNode::_pick_main_scene_custom_action(const String &p_custom_action_na
 	}
 }
 
-Ref<Texture2D> EditorNode::get_object_icon(const Object *p_object, const String &p_fallback) {
-	ERR_FAIL_COND_V(!p_object || !gui_base, nullptr);
+Ref<Texture2D> EditorNode::_get_class_or_script_icon(const String &p_class, const Ref<Script> &p_script, const String &p_fallback) {
+	ERR_FAIL_COND_V_MSG(p_class.is_empty(), nullptr, "Class name cannot be empty.");
+	EditorData &ed = EditorNode::get_editor_data();
 
-	Ref<Script> scr = p_object->get_script();
-	if (scr.is_null() && p_object->is_class("Script")) {
-		scr = p_object;
-	}
+	// Check for a script icon first.
+	if (p_script.is_valid()) {
+		Ref<Texture2D> script_icon = ed.get_script_icon(p_script);
+		if (script_icon.is_valid()) {
+			return script_icon;
+		}
 
-	if (scr.is_valid() && !script_icon_cache.has(scr)) {
-		Ref<Script> base_scr = scr;
-		while (base_scr.is_valid()) {
-			StringName name = EditorNode::get_editor_data().script_class_get_name(base_scr->get_path());
-			String icon_path = EditorNode::get_editor_data().script_class_get_icon_path(name);
-			Ref<ImageTexture> icon = _load_custom_class_icon(icon_path);
-			if (icon.is_valid()) {
-				script_icon_cache[scr] = icon;
-				return icon;
-			}
+		// No custom icon was found in the inheritance chain, so check the base
+		// class of the script instead.
+		String base_type;
+		p_script->get_language()->get_global_class_name(p_script->get_path(), &base_type);
 
-			// TODO: should probably be deprecated in 4.x
-			StringName base = base_scr->get_instance_base_type();
-			if (base != StringName() && EditorNode::get_editor_data().get_custom_types().has(base)) {
-				const Vector<EditorData::CustomType> &types = EditorNode::get_editor_data().get_custom_types()[base];
-				for (int i = 0; i < types.size(); ++i) {
-					if (types[i].script == base_scr && types[i].icon.is_valid()) {
-						script_icon_cache[scr] = types[i].icon;
-						return types[i].icon;
-					}
-				}
-			}
-			base_scr = base_scr->get_base_script();
+		// Check if the base type is an extension-defined type.
+		Ref<Texture2D> ext_icon = ed.extension_class_get_icon(base_type);
+		if (ext_icon.is_valid()) {
+			return ext_icon;
 		}
 
-		// If no icon found, cache it as null.
-		script_icon_cache[scr] = Ref<Texture>();
-	} else if (scr.is_valid() && script_icon_cache.has(scr) && script_icon_cache[scr].is_valid()) {
-		return script_icon_cache[scr];
+		// Look for the base type in the editor theme.
+		// This is only relevant for built-in classes.
+		if (gui_base && gui_base->has_theme_icon(base_type, "EditorIcons")) {
+			return gui_base->get_theme_icon(base_type, "EditorIcons");
+		}
 	}
 
-	// TODO: Should probably be deprecated in 4.x.
-	if (p_object->has_meta("_editor_icon")) {
-		return p_object->get_meta("_editor_icon");
+	// Script was not valid or didn't yield any useful values, try the class name
+	// directly.
+
+	// Check if the class name is an extension-defined type.
+	Ref<Texture2D> ext_icon = ed.extension_class_get_icon(p_class);
+	if (ext_icon.is_valid()) {
+		return ext_icon;
 	}
 
-	if (gui_base->has_theme_icon(p_object->get_class(), SNAME("EditorIcons"))) {
-		return gui_base->get_theme_icon(p_object->get_class(), SNAME("EditorIcons"));
+	// Check if the class name is a custom type.
+	// TODO: Should probably be deprecated in 4.x
+	const EditorData::CustomType *ctype = ed.get_custom_type_by_name(p_class);
+	if (ctype && ctype->icon.is_valid()) {
+		return ctype->icon;
 	}
 
-	if (p_fallback.length()) {
-		return gui_base->get_theme_icon(p_fallback, SNAME("EditorIcons"));
+	// Look up the class name or the fallback name in the editor theme.
+	// This is only relevant for built-in classes.
+	if (gui_base) {
+		if (gui_base->has_theme_icon(p_class, SNAME("EditorIcons"))) {
+			return gui_base->get_theme_icon(p_class, SNAME("EditorIcons"));
+		}
+
+		if (p_fallback.length() && gui_base->has_theme_icon(p_fallback, SNAME("EditorIcons"))) {
+			return gui_base->get_theme_icon(p_fallback, SNAME("EditorIcons"));
+		}
 	}
 
 	return nullptr;
 }
 
-Ref<Texture2D> EditorNode::get_class_icon(const String &p_class, const String &p_fallback) const {
-	ERR_FAIL_COND_V_MSG(p_class.is_empty(), nullptr, "Class name cannot be empty.");
+Ref<Texture2D> EditorNode::get_object_icon(const Object *p_object, const String &p_fallback) {
+	ERR_FAIL_NULL_V_MSG(p_object, nullptr, "Object cannot be null.");
 
-	if (ScriptServer::is_global_class(p_class)) {
-		String class_name = p_class;
-		Ref<Script> scr = EditorNode::get_editor_data().script_class_load_script(class_name);
-
-		while (true) {
-			String icon_path = EditorNode::get_editor_data().script_class_get_icon_path(class_name);
-			Ref<Texture> icon = _load_custom_class_icon(icon_path);
-			if (icon.is_valid()) {
-				return icon; // Current global class has icon.
-			}
-
-			// Find next global class along the inheritance chain.
-			do {
-				Ref<Script> base_scr = scr->get_base_script();
-				if (base_scr.is_null()) {
-					// We've reached a native class, use its icon.
-					String base_type;
-					scr->get_language()->get_global_class_name(scr->get_path(), &base_type);
-					if (gui_base->has_theme_icon(base_type, "EditorIcons")) {
-						return gui_base->get_theme_icon(base_type, "EditorIcons");
-					}
-					return gui_base->get_theme_icon(p_fallback, "EditorIcons");
-				}
-				scr = base_scr;
-				class_name = EditorNode::get_editor_data().script_class_get_name(scr->get_path());
-			} while (class_name.is_empty());
-		}
+	Ref<Script> scr = p_object->get_script();
+	if (scr.is_null() && p_object->is_class("Script")) {
+		scr = p_object;
 	}
 
-	if (const EditorData::CustomType *ctype = EditorNode::get_editor_data().get_custom_type_by_name(p_class)) {
-		return ctype->icon;
-	}
+	return _get_class_or_script_icon(p_object->get_class(), scr, p_fallback);
+}
 
-	if (gui_base->has_theme_icon(p_class, SNAME("EditorIcons"))) {
-		return gui_base->get_theme_icon(p_class, SNAME("EditorIcons"));
-	}
+Ref<Texture2D> EditorNode::get_class_icon(const String &p_class, const String &p_fallback) {
+	ERR_FAIL_COND_V_MSG(p_class.is_empty(), nullptr, "Class name cannot be empty.");
 
-	if (p_fallback.length() && gui_base->has_theme_icon(p_fallback, SNAME("EditorIcons"))) {
-		return gui_base->get_theme_icon(p_fallback, SNAME("EditorIcons"));
+	Ref<Script> scr;
+	if (ScriptServer::is_global_class(p_class)) {
+		scr = EditorNode::get_editor_data().script_class_load_script(p_class);
 	}
 
-	return nullptr;
+	return _get_class_or_script_icon(p_class, scr, p_fallback);
 }
 
 bool EditorNode::is_object_of_custom_type(const Object *p_object, const StringName &p_class) {
@@ -7159,6 +7125,7 @@ EditorNode::EditorNode() {
 	scene_tabs->set_select_with_rmb(true);
 	scene_tabs->add_tab("unsaved");
 	scene_tabs->set_tab_close_display_policy((TabBar::CloseButtonDisplayPolicy)EDITOR_GET("interface/scene_tabs/display_close_button").operator int());
+	scene_tabs->add_theme_constant_override("icon_max_width", gui_base->get_theme_constant(SNAME("class_icon_size"), SNAME("Editor")));
 	scene_tabs->set_max_tab_width(int(EDITOR_GET("interface/scene_tabs/maximum_width")) * EDSCALE);
 	scene_tabs->set_drag_to_rearrange_enabled(true);
 	scene_tabs->set_auto_translate(false);

+ 3 - 3
editor/editor_node.h

@@ -513,7 +513,6 @@ private:
 	PrintHandlerList print_handler;
 
 	HashMap<String, Ref<Texture2D>> icon_type_cache;
-	HashMap<Ref<Script>, Ref<Texture>> script_icon_cache;
 
 	static EditorBuildCallback build_callbacks[MAX_BUILD_CALLBACKS];
 	static EditorPluginInitializeCallback plugin_init_callbacks[MAX_INIT_CALLBACKS];
@@ -697,7 +696,8 @@ private:
 
 	void _feature_profile_changed();
 	bool _is_class_editor_disabled_by_feature_profile(const StringName &p_class);
-	Ref<ImageTexture> _load_custom_class_icon(const String &p_path) const;
+
+	Ref<Texture2D> _get_class_or_script_icon(const String &p_class, const Ref<Script> &p_script, const String &p_fallback = "Object");
 
 	void _pick_main_scene_custom_action(const String &p_custom_action_name);
 
@@ -879,7 +879,7 @@ public:
 	Ref<Script> get_object_custom_type_base(const Object *p_object) const;
 	StringName get_object_custom_type_name(const Object *p_object) const;
 	Ref<Texture2D> get_object_icon(const Object *p_object, const String &p_fallback = "Object");
-	Ref<Texture2D> get_class_icon(const String &p_class, const String &p_fallback = "Object") const;
+	Ref<Texture2D> get_class_icon(const String &p_class, const String &p_fallback = "Object");
 
 	bool is_object_of_custom_type(const Object *p_object, const StringName &p_class);
 

+ 7 - 2
editor/editor_path.cpp

@@ -201,8 +201,12 @@ void EditorPath::_notification(int p_what) {
 		case NOTIFICATION_THEME_CHANGED: {
 			update_path();
 
-			sub_objects_icon->set_texture(get_theme_icon(SNAME("arrow"), SNAME("OptionButton")));
+			int icon_size = get_theme_constant(SNAME("class_icon_size"), SNAME("Editor"));
+
+			current_object_icon->set_custom_minimum_size(Size2(icon_size, icon_size));
 			current_object_label->add_theme_font_override("font", get_theme_font(SNAME("main"), SNAME("EditorFonts")));
+			sub_objects_icon->set_texture(get_theme_icon(SNAME("arrow"), SNAME("OptionButton")));
+			sub_objects_menu->add_theme_constant_override("icon_max_width", icon_size);
 		} break;
 
 		case NOTIFICATION_READY: {
@@ -227,7 +231,8 @@ EditorPath::EditorPath(EditorSelectionHistory *p_history) {
 	main_mc->add_child(main_hb);
 
 	current_object_icon = memnew(TextureRect);
-	current_object_icon->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED);
+	current_object_icon->set_stretch_mode(TextureRect::STRETCH_KEEP_ASPECT_CENTERED);
+	current_object_icon->set_expand_mode(TextureRect::EXPAND_IGNORE_SIZE);
 	main_hb->add_child(current_object_icon);
 
 	current_object_label = memnew(Label);

+ 2 - 3
editor/editor_quick_open.cpp

@@ -69,10 +69,9 @@ void EditorQuickOpen::_build_search_cache(EditorFileSystemDirectory *p_efsd) {
 	for (int i = 0; i < p_efsd->get_file_count(); i++) {
 		String file = p_efsd->get_file_path(i);
 		String engine_type = p_efsd->get_file_type(i);
-
 		String script_type = p_efsd->get_file_resource_script_class(i);
-
 		String actual_type = script_type.is_empty() ? engine_type : script_type;
+
 		// Iterate all possible base types.
 		for (String &parent_type : base_types) {
 			if (ClassDB::is_parent_class(engine_type, parent_type) || EditorNode::get_editor_data().script_class_is_parent(script_type, parent_type)) {
@@ -81,7 +80,7 @@ void EditorQuickOpen::_build_search_cache(EditorFileSystemDirectory *p_efsd) {
 				// Store refs to used icons.
 				String ext = file.get_extension();
 				if (!icons.has(ext)) {
-					icons.insert(ext, get_theme_icon((has_theme_icon(actual_type, SNAME("EditorIcons")) ? actual_type : "Object"), SNAME("EditorIcons")));
+					icons.insert(ext, EditorNode::get_singleton()->get_class_icon(actual_type, "Object"));
 				}
 
 				// Stop testing base types as soon as we got a match.

+ 2 - 0
editor/editor_resource_picker.cpp

@@ -784,6 +784,7 @@ void EditorResourcePicker::_notification(int p_what) {
 			[[fallthrough]];
 		}
 		case NOTIFICATION_THEME_CHANGED: {
+			assign_button->add_theme_constant_override("icon_max_width", get_theme_constant(SNAME("class_icon_size"), SNAME("Editor")));
 			edit_button->set_icon(get_theme_icon(SNAME("select_arrow"), SNAME("Tree")));
 		} break;
 
@@ -923,6 +924,7 @@ EditorResourcePicker::EditorResourcePicker(bool p_hide_assign_button_controls) {
 	assign_button = memnew(Button);
 	assign_button->set_flat(true);
 	assign_button->set_h_size_flags(SIZE_EXPAND_FILL);
+	assign_button->set_expand_icon(true);
 	assign_button->set_clip_text(true);
 	assign_button->set_auto_translate(false);
 	SET_DRAG_FORWARDING_GCD(assign_button, EditorResourcePicker);

+ 2 - 0
editor/editor_themes.cpp

@@ -587,9 +587,11 @@ Ref<Theme> create_editor_theme(const Ref<Theme> p_theme) {
 	} else {
 		theme->set_color("highend_color", "Editor", Color(1.0, 0.0, 0.0));
 	}
+
 	const int thumb_size = EDITOR_GET("filesystem/file_dialog/thumbnail_size");
 	theme->set_constant("scale", "Editor", EDSCALE);
 	theme->set_constant("thumb_size", "Editor", thumb_size);
+	theme->set_constant("class_icon_size", "Editor", 16 * EDSCALE);
 	theme->set_constant("dark_theme", "Editor", dark_theme);
 	theme->set_constant("color_picker_button_height", "Editor", 28 * EDSCALE);
 

+ 22 - 13
editor/scene_tree_dock.cpp

@@ -1278,13 +1278,6 @@ void SceneTreeDock::_notification(int p_what) {
 			spatial_editor_plugin->get_spatial_editor()->connect("item_lock_status_changed", callable_mp(scene_tree, &SceneTreeEditor::_update_tree).bind(false));
 			spatial_editor_plugin->get_spatial_editor()->connect("item_group_status_changed", callable_mp(scene_tree, &SceneTreeEditor::_update_tree).bind(false));
 
-			button_add->set_icon(get_theme_icon(SNAME("Add"), SNAME("EditorIcons")));
-			button_instance->set_icon(get_theme_icon(SNAME("Instance"), SNAME("EditorIcons")));
-			button_create_script->set_icon(get_theme_icon(SNAME("ScriptCreate"), SNAME("EditorIcons")));
-			button_detach_script->set_icon(get_theme_icon(SNAME("ScriptRemove"), SNAME("EditorIcons")));
-			button_tree_menu->set_icon(get_theme_icon(SNAME("GuiTabMenuHl"), SNAME("EditorIcons")));
-
-			filter->set_right_icon(get_theme_icon(SNAME("Search"), SNAME("EditorIcons")));
 			filter->set_clear_button_enabled(true);
 
 			// create_root_dialog
@@ -1366,19 +1359,35 @@ void SceneTreeDock::_notification(int p_what) {
 
 		case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {
 			scene_tree->set_auto_expand_selected(EDITOR_GET("docks/scene_tree/auto_expand_to_selected"), false);
+		} break;
+
+		case NOTIFICATION_THEME_CHANGED: {
 			button_add->set_icon(get_theme_icon(SNAME("Add"), SNAME("EditorIcons")));
 			button_instance->set_icon(get_theme_icon(SNAME("Instance"), SNAME("EditorIcons")));
 			button_create_script->set_icon(get_theme_icon(SNAME("ScriptCreate"), SNAME("EditorIcons")));
 			button_detach_script->set_icon(get_theme_icon(SNAME("ScriptRemove"), SNAME("EditorIcons")));
 			button_tree_menu->set_icon(get_theme_icon(SNAME("GuiTabMenuHl"), SNAME("EditorIcons")));
-			button_2d->set_icon(get_theme_icon(SNAME("Node2D"), SNAME("EditorIcons")));
-			button_3d->set_icon(get_theme_icon(SNAME("Node3D"), SNAME("EditorIcons")));
-			button_ui->set_icon(get_theme_icon(SNAME("Control"), SNAME("EditorIcons")));
-			button_custom->set_icon(get_theme_icon(SNAME("Add"), SNAME("EditorIcons")));
-			button_clipboard->set_icon(get_theme_icon(SNAME("ActionPaste"), SNAME("EditorIcons")));
 
 			filter->set_right_icon(get_theme_icon(SNAME("Search"), SNAME("EditorIcons")));
-			filter->set_clear_button_enabled(true);
+
+			// These buttons are created on READY, because reasons...
+			if (button_2d) {
+				button_2d->set_icon(get_theme_icon(SNAME("Node2D"), SNAME("EditorIcons")));
+			}
+			if (button_3d) {
+				button_3d->set_icon(get_theme_icon(SNAME("Node3D"), SNAME("EditorIcons")));
+			}
+			if (button_ui) {
+				button_ui->set_icon(get_theme_icon(SNAME("Control"), SNAME("EditorIcons")));
+			}
+			if (button_custom) {
+				button_custom->set_icon(get_theme_icon(SNAME("Add"), SNAME("EditorIcons")));
+			}
+			if (button_clipboard) {
+				button_clipboard->set_icon(get_theme_icon(SNAME("ActionPaste"), SNAME("EditorIcons")));
+			}
+
+			menu_subresources->add_theme_constant_override("icon_max_width", get_theme_constant(SNAME("class_icon_size"), SNAME("Editor")));
 		} break;
 
 		case NOTIFICATION_PROCESS: {

+ 2 - 0
editor/scene_tree_editor.cpp

@@ -878,6 +878,8 @@ void SceneTreeEditor::_notification(int p_what) {
 		} break;
 
 		case NOTIFICATION_THEME_CHANGED: {
+			tree->add_theme_constant_override("icon_max_width", get_theme_constant(SNAME("class_icon_size"), SNAME("Editor")));
+
 			_update_tree();
 		} break;
 	}

+ 19 - 4
scene/gui/button.cpp

@@ -83,6 +83,7 @@ void Button::_update_theme_item_cache() {
 	theme_cache.icon = get_theme_icon(SNAME("icon"));
 
 	theme_cache.h_separation = get_theme_constant(SNAME("h_separation"));
+	theme_cache.icon_max_width = get_theme_constant(SNAME("icon_max_width"));
 }
 
 void Button::_notification(int p_what) {
@@ -252,7 +253,6 @@ void Button::_notification(int p_what) {
 
 				float icon_ofs_region = 0.0;
 				Point2 style_offset;
-				Size2 icon_size = _icon->get_size();
 				if (icon_align_rtl_checked == HORIZONTAL_ALIGNMENT_LEFT) {
 					style_offset.x = style->get_margin(SIDE_LEFT);
 					if (_internal_margin[SIDE_LEFT] > 0) {
@@ -268,6 +268,7 @@ void Button::_notification(int p_what) {
 				}
 				style_offset.y = style->get_margin(SIDE_TOP);
 
+				Size2 icon_size = _icon->get_size();
 				if (expand_icon) {
 					Size2 _size = get_size() - style->get_offset() * 2;
 					int icon_text_separation = text.is_empty() ? 0 : theme_cache.h_separation;
@@ -285,6 +286,7 @@ void Button::_notification(int p_what) {
 
 					icon_size = Size2(icon_width, icon_height);
 				}
+				icon_size = _fit_icon_size(icon_size);
 
 				if (icon_align_rtl_checked == HORIZONTAL_ALIGNMENT_LEFT) {
 					icon_region = Rect2(style_offset + Point2(icon_ofs_region, Math::floor((valign - icon_size.y) * 0.5)), icon_size);
@@ -365,6 +367,18 @@ void Button::_notification(int p_what) {
 	}
 }
 
+Size2 Button::_fit_icon_size(const Size2 &p_size) const {
+	int max_width = theme_cache.icon_max_width;
+	Size2 icon_size = p_size;
+
+	if (max_width > 0 && icon_size.width > max_width) {
+		icon_size.height = icon_size.height * max_width / icon_size.width;
+		icon_size.width = max_width;
+	}
+
+	return icon_size;
+}
+
 Size2 Button::get_minimum_size_for_text_and_icon(const String &p_text, Ref<Texture2D> p_icon) const {
 	Ref<TextParagraph> paragraph;
 	if (p_text.is_empty()) {
@@ -380,15 +394,16 @@ Size2 Button::get_minimum_size_for_text_and_icon(const String &p_text, Ref<Textu
 	}
 
 	if (!expand_icon && p_icon.is_valid()) {
-		minsize.height = MAX(minsize.height, p_icon->get_height());
+		Size2 icon_size = _fit_icon_size(p_icon->get_size());
+		minsize.height = MAX(minsize.height, icon_size.height);
 
 		if (icon_alignment != HORIZONTAL_ALIGNMENT_CENTER) {
-			minsize.width += p_icon->get_width();
+			minsize.width += icon_size.width;
 			if (!xl_text.is_empty() || !p_text.is_empty()) {
 				minsize.width += MAX(0, theme_cache.h_separation);
 			}
 		} else {
-			minsize.width = MAX(minsize.width, p_icon->get_width());
+			minsize.width = MAX(minsize.width, icon_size.width);
 		}
 	}
 

+ 3 - 0
scene/gui/button.h

@@ -89,8 +89,11 @@ private:
 		Ref<Texture2D> icon;
 
 		int h_separation = 0;
+		int icon_max_width = 0;
 	} theme_cache;
 
+	Size2 _fit_icon_size(const Size2 &p_size) const;
+
 	void _shape(Ref<TextParagraph> p_paragraph = Ref<TextParagraph>(), String p_text = "");
 
 protected:

+ 88 - 8
scene/gui/popup_menu.cpp

@@ -47,6 +47,26 @@ String PopupMenu::_get_accel_text(const Item &p_item) const {
 	return String();
 }
 
+Size2 PopupMenu::_get_item_icon_size(int p_item) const {
+	const PopupMenu::Item &item = items[p_item];
+	Size2 icon_size = item.get_icon_size();
+
+	int max_width = 0;
+	if (theme_cache.icon_max_width > 0) {
+		max_width = theme_cache.icon_max_width;
+	}
+	if (item.icon_max_width > 0 && (max_width == 0 || item.icon_max_width < max_width)) {
+		max_width = item.icon_max_width;
+	}
+
+	if (max_width > 0 && icon_size.width > max_width) {
+		icon_size.height = icon_size.height * max_width / icon_size.width;
+		icon_size.width = max_width;
+	}
+
+	return icon_size;
+}
+
 Size2 PopupMenu::_get_contents_minimum_size() const {
 	Size2 minsize = theme_cache.panel_style->get_minimum_size(); // Accounts for margin in the margin container
 	minsize.x += scroll_container->get_v_scroll_bar()->get_size().width * 2; // Adds a buffer so that the scrollbar does not render over the top of content
@@ -61,7 +81,7 @@ Size2 PopupMenu::_get_contents_minimum_size() const {
 		Size2 item_size;
 		const_cast<PopupMenu *>(this)->_shape_item(i);
 
-		Size2 icon_size = items[i].get_icon_size();
+		Size2 icon_size = _get_item_icon_size(i);
 		item_size.height = _get_item_height(i);
 		icon_w = MAX(icon_size.width, icon_w);
 
@@ -109,7 +129,8 @@ Size2 PopupMenu::_get_contents_minimum_size() const {
 int PopupMenu::_get_item_height(int p_item) const {
 	ERR_FAIL_INDEX_V(p_item, items.size(), 0);
 
-	int icon_height = items[p_item].get_icon_size().height;
+	Size2 icon_size = _get_item_icon_size(p_item);
+	int icon_height = icon_size.height;
 	if (items[p_item].checkable_type && !items[p_item].separator) {
 		icon_height = MAX(icon_height, MAX(theme_cache.checked->get_height(), theme_cache.radio_checked->get_height()));
 	}
@@ -540,7 +561,8 @@ void PopupMenu::_draw_items() {
 			continue;
 		}
 
-		icon_ofs = MAX(items[i].get_icon_size().width, icon_ofs);
+		Size2 icon_size = _get_item_icon_size(i);
+		icon_ofs = MAX(icon_size.width, icon_ofs);
 
 		if (items[i].checkable_type) {
 			has_check = true;
@@ -569,7 +591,7 @@ void PopupMenu::_draw_items() {
 		_shape_item(i);
 
 		Point2 item_ofs = ofs;
-		Size2 icon_size = items[i].get_icon_size();
+		Size2 icon_size = _get_item_icon_size(i);
 		float h = _get_item_height(i);
 
 		if (i == mouse_over) {
@@ -631,21 +653,26 @@ void PopupMenu::_draw_items() {
 
 		// Icon
 		if (!items[i].icon.is_null()) {
+			const Point2 icon_offset = Point2(0, Math::floor((h - icon_size.height) / 2.0));
+			Point2 icon_pos;
+
 			if (items[i].separator) {
 				separator_ofs -= (icon_size.width + theme_cache.h_separation) / 2;
 
 				if (rtl) {
-					items[i].icon->draw(ci, Size2(control->get_size().width - item_ofs.x - separator_ofs - icon_size.width, item_ofs.y) + Point2(0, Math::floor((h - icon_size.height) / 2.0)), icon_color);
+					icon_pos = Size2(control->get_size().width - item_ofs.x - separator_ofs - icon_size.width, item_ofs.y);
 				} else {
-					items[i].icon->draw(ci, item_ofs + Size2(separator_ofs, 0) + Point2(0, Math::floor((h - icon_size.height) / 2.0)), icon_color);
+					icon_pos = item_ofs + Size2(separator_ofs, 0);
 				}
 			} else {
 				if (rtl) {
-					items[i].icon->draw(ci, Size2(control->get_size().width - item_ofs.x - check_ofs - icon_size.width, item_ofs.y) + Point2(0, Math::floor((h - icon_size.height) / 2.0)), icon_color);
+					icon_pos = Size2(control->get_size().width - item_ofs.x - check_ofs - icon_size.width, item_ofs.y);
 				} else {
-					items[i].icon->draw(ci, item_ofs + Size2(check_ofs, 0) + Point2(0, Math::floor((h - icon_size.height) / 2.0)), icon_color);
+					icon_pos = item_ofs + Size2(check_ofs, 0);
 				}
 			}
+
+			items[i].icon->draw_rect(ci, Rect2(icon_pos + icon_offset, icon_size), false, icon_color);
 		}
 
 		// Submenu arrow on right hand side.
@@ -802,6 +829,7 @@ void PopupMenu::_update_theme_item_cache() {
 	theme_cache.indent = get_theme_constant(SNAME("indent"));
 	theme_cache.item_start_padding = get_theme_constant(SNAME("item_start_padding"));
 	theme_cache.item_end_padding = get_theme_constant(SNAME("item_end_padding"));
+	theme_cache.icon_max_width = get_theme_constant(SNAME("icon_max_width"));
 
 	theme_cache.checked = get_theme_icon(SNAME("checked"));
 	theme_cache.checked_disabled = get_theme_icon(SNAME("checked_disabled"));
@@ -946,8 +974,10 @@ void PopupMenu::add_item(const String &p_label, int p_id, Key p_accel) {
 	Item item;
 	ITEM_SETUP_WITH_ACCEL(p_label, p_id, p_accel);
 	items.push_back(item);
+
 	_shape_item(items.size() - 1);
 	control->queue_redraw();
+
 	child_controls_changed();
 	notify_property_list_changed();
 	_menu_changed();
@@ -958,8 +988,10 @@ void PopupMenu::add_icon_item(const Ref<Texture2D> &p_icon, const String &p_labe
 	ITEM_SETUP_WITH_ACCEL(p_label, p_id, p_accel);
 	item.icon = p_icon;
 	items.push_back(item);
+
 	_shape_item(items.size() - 1);
 	control->queue_redraw();
+
 	child_controls_changed();
 	notify_property_list_changed();
 	_menu_changed();
@@ -970,8 +1002,10 @@ void PopupMenu::add_check_item(const String &p_label, int p_id, Key p_accel) {
 	ITEM_SETUP_WITH_ACCEL(p_label, p_id, p_accel);
 	item.checkable_type = Item::CHECKABLE_TYPE_CHECK_BOX;
 	items.push_back(item);
+
 	_shape_item(items.size() - 1);
 	control->queue_redraw();
+
 	child_controls_changed();
 	_menu_changed();
 }
@@ -982,8 +1016,10 @@ void PopupMenu::add_icon_check_item(const Ref<Texture2D> &p_icon, const String &
 	item.icon = p_icon;
 	item.checkable_type = Item::CHECKABLE_TYPE_CHECK_BOX;
 	items.push_back(item);
+
 	_shape_item(items.size() - 1);
 	control->queue_redraw();
+
 	child_controls_changed();
 }
 
@@ -992,8 +1028,10 @@ void PopupMenu::add_radio_check_item(const String &p_label, int p_id, Key p_acce
 	ITEM_SETUP_WITH_ACCEL(p_label, p_id, p_accel);
 	item.checkable_type = Item::CHECKABLE_TYPE_RADIO_BUTTON;
 	items.push_back(item);
+
 	_shape_item(items.size() - 1);
 	control->queue_redraw();
+
 	child_controls_changed();
 	_menu_changed();
 }
@@ -1004,8 +1042,10 @@ void PopupMenu::add_icon_radio_check_item(const Ref<Texture2D> &p_icon, const St
 	item.icon = p_icon;
 	item.checkable_type = Item::CHECKABLE_TYPE_RADIO_BUTTON;
 	items.push_back(item);
+
 	_shape_item(items.size() - 1);
 	control->queue_redraw();
+
 	child_controls_changed();
 	_menu_changed();
 }
@@ -1016,8 +1056,10 @@ void PopupMenu::add_multistate_item(const String &p_label, int p_max_states, int
 	item.max_states = p_max_states;
 	item.state = p_default_state;
 	items.push_back(item);
+
 	_shape_item(items.size() - 1);
 	control->queue_redraw();
+
 	child_controls_changed();
 	_menu_changed();
 }
@@ -1035,8 +1077,10 @@ void PopupMenu::add_shortcut(const Ref<Shortcut> &p_shortcut, int p_id, bool p_g
 	Item item;
 	ITEM_SETUP_WITH_SHORTCUT(p_shortcut, p_id, p_global);
 	items.push_back(item);
+
 	_shape_item(items.size() - 1);
 	control->queue_redraw();
+
 	child_controls_changed();
 	_menu_changed();
 }
@@ -1046,8 +1090,10 @@ void PopupMenu::add_icon_shortcut(const Ref<Texture2D> &p_icon, const Ref<Shortc
 	ITEM_SETUP_WITH_SHORTCUT(p_shortcut, p_id, p_global);
 	item.icon = p_icon;
 	items.push_back(item);
+
 	_shape_item(items.size() - 1);
 	control->queue_redraw();
+
 	child_controls_changed();
 	_menu_changed();
 }
@@ -1057,8 +1103,10 @@ void PopupMenu::add_check_shortcut(const Ref<Shortcut> &p_shortcut, int p_id, bo
 	ITEM_SETUP_WITH_SHORTCUT(p_shortcut, p_id, p_global);
 	item.checkable_type = Item::CHECKABLE_TYPE_CHECK_BOX;
 	items.push_back(item);
+
 	_shape_item(items.size() - 1);
 	control->queue_redraw();
+
 	child_controls_changed();
 	_menu_changed();
 }
@@ -1069,8 +1117,10 @@ void PopupMenu::add_icon_check_shortcut(const Ref<Texture2D> &p_icon, const Ref<
 	item.icon = p_icon;
 	item.checkable_type = Item::CHECKABLE_TYPE_CHECK_BOX;
 	items.push_back(item);
+
 	_shape_item(items.size() - 1);
 	control->queue_redraw();
+
 	child_controls_changed();
 	_menu_changed();
 }
@@ -1080,8 +1130,10 @@ void PopupMenu::add_radio_check_shortcut(const Ref<Shortcut> &p_shortcut, int p_
 	ITEM_SETUP_WITH_SHORTCUT(p_shortcut, p_id, p_global);
 	item.checkable_type = Item::CHECKABLE_TYPE_RADIO_BUTTON;
 	items.push_back(item);
+
 	_shape_item(items.size() - 1);
 	control->queue_redraw();
+
 	child_controls_changed();
 	_menu_changed();
 }
@@ -1092,8 +1144,10 @@ void PopupMenu::add_icon_radio_check_shortcut(const Ref<Texture2D> &p_icon, cons
 	item.icon = p_icon;
 	item.checkable_type = Item::CHECKABLE_TYPE_RADIO_BUTTON;
 	items.push_back(item);
+
 	_shape_item(items.size() - 1);
 	control->queue_redraw();
+
 	child_controls_changed();
 	_menu_changed();
 }
@@ -1105,8 +1159,10 @@ void PopupMenu::add_submenu_item(const String &p_label, const String &p_submenu,
 	item.id = p_id == -1 ? items.size() : p_id;
 	item.submenu = p_submenu;
 	items.push_back(item);
+
 	_shape_item(items.size() - 1);
 	control->queue_redraw();
+
 	child_controls_changed();
 	_menu_changed();
 }
@@ -1176,6 +1232,23 @@ void PopupMenu::set_item_icon(int p_idx, const Ref<Texture2D> &p_icon) {
 	_menu_changed();
 }
 
+void PopupMenu::set_item_icon_max_width(int p_idx, int p_width) {
+	if (p_idx < 0) {
+		p_idx += get_item_count();
+	}
+	ERR_FAIL_INDEX(p_idx, items.size());
+
+	if (items[p_idx].icon_max_width == p_width) {
+		return;
+	}
+
+	items.write[p_idx].icon_max_width = p_width;
+
+	control->queue_redraw();
+	child_controls_changed();
+	_menu_changed();
+}
+
 void PopupMenu::set_item_checked(int p_idx, bool p_checked) {
 	if (p_idx < 0) {
 		p_idx += get_item_count();
@@ -1314,6 +1387,11 @@ Ref<Texture2D> PopupMenu::get_item_icon(int p_idx) const {
 	return items[p_idx].icon;
 }
 
+int PopupMenu::get_item_icon_max_width(int p_idx) const {
+	ERR_FAIL_INDEX_V(p_idx, items.size(), 0);
+	return items[p_idx].icon_max_width;
+}
+
 Key PopupMenu::get_item_accelerator(int p_idx) const {
 	ERR_FAIL_INDEX_V(p_idx, items.size(), Key::NONE);
 	return items[p_idx].accel;
@@ -2023,6 +2101,7 @@ void PopupMenu::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("set_item_text_direction", "index", "direction"), &PopupMenu::set_item_text_direction);
 	ClassDB::bind_method(D_METHOD("set_item_language", "index", "language"), &PopupMenu::set_item_language);
 	ClassDB::bind_method(D_METHOD("set_item_icon", "index", "icon"), &PopupMenu::set_item_icon);
+	ClassDB::bind_method(D_METHOD("set_item_icon_max_width", "index", "width"), &PopupMenu::set_item_icon_max_width);
 	ClassDB::bind_method(D_METHOD("set_item_checked", "index", "checked"), &PopupMenu::set_item_checked);
 	ClassDB::bind_method(D_METHOD("set_item_id", "index", "id"), &PopupMenu::set_item_id);
 	ClassDB::bind_method(D_METHOD("set_item_accelerator", "index", "accel"), &PopupMenu::set_item_accelerator);
@@ -2045,6 +2124,7 @@ void PopupMenu::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("get_item_text_direction", "index"), &PopupMenu::get_item_text_direction);
 	ClassDB::bind_method(D_METHOD("get_item_language", "index"), &PopupMenu::get_item_language);
 	ClassDB::bind_method(D_METHOD("get_item_icon", "index"), &PopupMenu::get_item_icon);
+	ClassDB::bind_method(D_METHOD("get_item_icon_max_width", "index"), &PopupMenu::get_item_icon_max_width);
 	ClassDB::bind_method(D_METHOD("is_item_checked", "index"), &PopupMenu::is_item_checked);
 	ClassDB::bind_method(D_METHOD("get_item_id", "index"), &PopupMenu::get_item_id);
 	ClassDB::bind_method(D_METHOD("get_item_index", "id"), &PopupMenu::get_item_index);

+ 5 - 0
scene/gui/popup_menu.h

@@ -42,6 +42,7 @@ class PopupMenu : public Popup {
 
 	struct Item {
 		Ref<Texture2D> icon;
+		int icon_max_width = 0;
 		String text;
 		String xl_text;
 		Ref<TextLine> text_buf;
@@ -103,6 +104,7 @@ class PopupMenu : public Popup {
 
 	int _get_item_height(int p_item) const;
 	int _get_items_total_height() const;
+	Size2 _get_item_icon_size(int p_item) const;
 
 	void _shape_item(int p_item);
 
@@ -144,6 +146,7 @@ class PopupMenu : public Popup {
 		int indent = 0;
 		int item_start_padding = 0;
 		int item_end_padding = 0;
+		int icon_max_width = 0;
 
 		Ref<Texture2D> checked;
 		Ref<Texture2D> checked_disabled;
@@ -222,6 +225,7 @@ public:
 	void set_item_text_direction(int p_idx, Control::TextDirection p_text_direction);
 	void set_item_language(int p_idx, const String &p_language);
 	void set_item_icon(int p_idx, const Ref<Texture2D> &p_icon);
+	void set_item_icon_max_width(int p_idx, int p_width);
 	void set_item_checked(int p_idx, bool p_checked);
 	void set_item_id(int p_idx, int p_id);
 	void set_item_accelerator(int p_idx, Key p_accel);
@@ -245,6 +249,7 @@ public:
 	String get_item_language(int p_idx) const;
 	int get_item_idx_from_text(const String &text) const;
 	Ref<Texture2D> get_item_icon(int p_idx) const;
+	int get_item_icon_max_width(int p_idx) const;
 	bool is_item_checked(int p_idx) const;
 	int get_item_id(int p_idx) const;
 	int get_item_index(int p_id) const;

+ 64 - 10
scene/gui/tab_bar.cpp

@@ -63,10 +63,10 @@ Size2 TabBar::get_minimum_size() const {
 		}
 		ms.width += style->get_minimum_size().width;
 
-		Ref<Texture2D> tex = tabs[i].icon;
-		if (tex.is_valid()) {
-			ms.height = MAX(ms.height, tex->get_size().height + y_margin);
-			ms.width += tex->get_size().width + theme_cache.h_separation;
+		if (tabs[i].icon.is_valid()) {
+			const Size2 icon_size = _get_tab_icon_size(i);
+			ms.height = MAX(ms.height, icon_size.height + y_margin);
+			ms.width += icon_size.width + theme_cache.h_separation;
 		}
 
 		if (!tabs[i].text.is_empty()) {
@@ -304,6 +304,7 @@ void TabBar::_update_theme_item_cache() {
 	Control::_update_theme_item_cache();
 
 	theme_cache.h_separation = get_theme_constant(SNAME("h_separation"));
+	theme_cache.icon_max_width = get_theme_constant(SNAME("icon_max_width"));
 
 	theme_cache.tab_unselected_style = get_theme_stylebox(SNAME("tab_unselected"));
 	theme_cache.tab_selected_style = get_theme_stylebox(SNAME("tab_selected"));
@@ -492,9 +493,11 @@ void TabBar::_draw_tab(Ref<StyleBox> &p_tab_style, Color &p_font_color, int p_in
 	// Draw the icon.
 	Ref<Texture2D> icon = tabs[p_index].icon;
 	if (icon.is_valid()) {
-		icon->draw(ci, Point2i(rtl ? p_x - icon->get_width() : p_x, p_tab_style->get_margin(SIDE_TOP) + ((sb_rect.size.y - sb_ms.y) - icon->get_height()) / 2));
+		const Size2 icon_size = _get_tab_icon_size(p_index);
+		const Point2 icon_pos = Point2i(rtl ? p_x - icon_size.width : p_x, p_tab_style->get_margin(SIDE_TOP) + ((sb_rect.size.y - sb_ms.y) - icon_size.height) / 2);
+		icon->draw_rect(ci, Rect2(icon_pos, icon_size));
 
-		p_x = rtl ? p_x - icon->get_width() - theme_cache.h_separation : p_x + icon->get_width() + theme_cache.h_separation;
+		p_x = rtl ? p_x - icon_size.width - theme_cache.h_separation : p_x + icon_size.width + theme_cache.h_separation;
 	}
 
 	// Draw the text.
@@ -719,6 +722,29 @@ Ref<Texture2D> TabBar::get_tab_icon(int p_tab) const {
 	return tabs[p_tab].icon;
 }
 
+void TabBar::set_tab_icon_max_width(int p_tab, int p_width) {
+	ERR_FAIL_INDEX(p_tab, tabs.size());
+
+	if (tabs[p_tab].icon_max_width == p_width) {
+		return;
+	}
+
+	tabs.write[p_tab].icon_max_width = p_width;
+
+	_update_cache();
+	_ensure_no_over_offset();
+	if (scroll_to_selected) {
+		ensure_tab_visible(current);
+	}
+	queue_redraw();
+	update_minimum_size();
+}
+
+int TabBar::get_tab_icon_max_width(int p_tab) const {
+	ERR_FAIL_INDEX_V(p_tab, tabs.size(), 0);
+	return tabs[p_tab].icon_max_width;
+}
+
 void TabBar::set_tab_disabled(int p_tab, bool p_disabled) {
 	ERR_FAIL_INDEX(p_tab, tabs.size());
 
@@ -1023,9 +1049,14 @@ Variant TabBar::get_drag_data(const Point2 &p_point) {
 	HBoxContainer *drag_preview = memnew(HBoxContainer);
 
 	if (!tabs[tab_over].icon.is_null()) {
+		const Size2 icon_size = _get_tab_icon_size(tab_over);
+
 		TextureRect *tf = memnew(TextureRect);
 		tf->set_texture(tabs[tab_over].icon);
-		tf->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED);
+		tf->set_stretch_mode(TextureRect::STRETCH_KEEP_ASPECT_CENTERED);
+		tf->set_expand_mode(TextureRect::EXPAND_IGNORE_SIZE);
+		tf->set_custom_minimum_size(icon_size);
+
 		drag_preview->add_child(tf);
 	}
 
@@ -1270,9 +1301,9 @@ int TabBar::get_tab_width(int p_idx) const {
 	}
 	int x = style->get_minimum_size().width;
 
-	Ref<Texture2D> tex = tabs[p_idx].icon;
-	if (tex.is_valid()) {
-		x += tex->get_width() + theme_cache.h_separation;
+	if (tabs[p_idx].icon.is_valid()) {
+		const Size2 icon_size = _get_tab_icon_size(p_idx);
+		x += icon_size.width + theme_cache.h_separation;
 	}
 
 	if (!tabs[p_idx].text.is_empty()) {
@@ -1305,6 +1336,27 @@ int TabBar::get_tab_width(int p_idx) const {
 	return x;
 }
 
+Size2 TabBar::_get_tab_icon_size(int p_index) const {
+	ERR_FAIL_INDEX_V(p_index, tabs.size(), Size2());
+	const TabBar::Tab &tab = tabs[p_index];
+	Size2 icon_size = tab.icon->get_size();
+
+	int icon_max_width = 0;
+	if (theme_cache.icon_max_width > 0) {
+		icon_max_width = theme_cache.icon_max_width;
+	}
+	if (tab.icon_max_width > 0 && (icon_max_width == 0 || tab.icon_max_width < icon_max_width)) {
+		icon_max_width = tab.icon_max_width;
+	}
+
+	if (icon_max_width > 0 && icon_size.width > icon_max_width) {
+		icon_size.height = icon_size.height * icon_max_width / icon_size.width;
+		icon_size.width = icon_max_width;
+	}
+
+	return icon_size;
+}
+
 void TabBar::_ensure_no_over_offset() {
 	if (!is_inside_tree() || !buttons_visible) {
 		return;
@@ -1547,6 +1599,8 @@ void TabBar::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("get_tab_language", "tab_idx"), &TabBar::get_tab_language);
 	ClassDB::bind_method(D_METHOD("set_tab_icon", "tab_idx", "icon"), &TabBar::set_tab_icon);
 	ClassDB::bind_method(D_METHOD("get_tab_icon", "tab_idx"), &TabBar::get_tab_icon);
+	ClassDB::bind_method(D_METHOD("set_tab_icon_max_width", "tab_idx", "width"), &TabBar::set_tab_icon_max_width);
+	ClassDB::bind_method(D_METHOD("get_tab_icon_max_width", "tab_idx"), &TabBar::get_tab_icon_max_width);
 	ClassDB::bind_method(D_METHOD("set_tab_button_icon", "tab_idx", "icon"), &TabBar::set_tab_button_icon);
 	ClassDB::bind_method(D_METHOD("get_tab_button_icon", "tab_idx"), &TabBar::get_tab_button_icon);
 	ClassDB::bind_method(D_METHOD("set_tab_disabled", "tab_idx", "disabled"), &TabBar::set_tab_disabled);

+ 7 - 0
scene/gui/tab_bar.h

@@ -62,6 +62,8 @@ private:
 
 		Ref<TextLine> text_buf;
 		Ref<Texture2D> icon;
+		int icon_max_width = 0;
+
 		bool disabled = false;
 		bool hidden = false;
 		int ofs_cache = 0;
@@ -106,6 +108,7 @@ private:
 
 	struct ThemeCache {
 		int h_separation = 0;
+		int icon_max_width = 0;
 
 		Ref<StyleBox> tab_unselected_style;
 		Ref<StyleBox> tab_selected_style;
@@ -133,6 +136,7 @@ private:
 	} theme_cache;
 
 	int get_tab_width(int p_idx) const;
+	Size2 _get_tab_icon_size(int p_idx) const;
 	void _ensure_no_over_offset();
 
 	void _update_hover();
@@ -171,6 +175,9 @@ public:
 	void set_tab_icon(int p_tab, const Ref<Texture2D> &p_icon);
 	Ref<Texture2D> get_tab_icon(int p_tab) const;
 
+	void set_tab_icon_max_width(int p_tab, int p_width);
+	int get_tab_icon_max_width(int p_tab) const;
+
 	void set_tab_disabled(int p_tab, bool p_disabled);
 	bool is_tab_disabled(int p_tab) const;
 

+ 2 - 0
scene/gui/tab_container.cpp

@@ -146,6 +146,7 @@ void TabContainer::_update_theme_item_cache() {
 
 	// TabBar overrides.
 	theme_cache.icon_separation = get_theme_constant(SNAME("icon_separation"));
+	theme_cache.icon_max_width = get_theme_constant(SNAME("icon_max_width"));
 	theme_cache.outline_size = get_theme_constant(SNAME("outline_size"));
 
 	theme_cache.tab_unselected_style = get_theme_stylebox(SNAME("tab_unselected"));
@@ -245,6 +246,7 @@ void TabContainer::_on_theme_changed() {
 	tab_bar->add_theme_font_size_override(SNAME("font_size"), theme_cache.tab_font_size);
 
 	tab_bar->add_theme_constant_override(SNAME("h_separation"), theme_cache.icon_separation);
+	tab_bar->add_theme_constant_override(SNAME("icon_max_width"), theme_cache.icon_max_width);
 	tab_bar->add_theme_constant_override(SNAME("outline_size"), theme_cache.outline_size);
 
 	_update_margins();

+ 1 - 0
scene/gui/tab_container.h

@@ -59,6 +59,7 @@ class TabContainer : public Container {
 
 		// TabBar overrides.
 		int icon_separation = 0;
+		int icon_max_width = 0;
 		int outline_size = 0;
 
 		Ref<StyleBox> tab_unselected_style;

+ 28 - 26
scene/gui/tree.cpp

@@ -1340,10 +1340,7 @@ Size2 TreeItem::get_minimum_size(int p_column) {
 			size.width += parent_tree->theme_cache.checked->get_width() + parent_tree->theme_cache.h_separation;
 		}
 		if (cell.icon.is_valid()) {
-			Size2i icon_size = cell.get_icon_size();
-			if (cell.icon_max_w > 0 && icon_size.width > cell.icon_max_w) {
-				icon_size.width = cell.icon_max_w;
-			}
+			Size2i icon_size = parent_tree->_get_cell_icon_size(cell);
 			size.width += icon_size.width + parent_tree->theme_cache.h_separation;
 			size.height = MAX(size.height, icon_size.height);
 		}
@@ -1628,6 +1625,7 @@ void Tree::_update_theme_item_cache() {
 	theme_cache.v_separation = get_theme_constant(SNAME("v_separation"));
 	theme_cache.item_margin = get_theme_constant(SNAME("item_margin"));
 	theme_cache.button_margin = get_theme_constant(SNAME("button_margin"));
+	theme_cache.icon_max_width = get_theme_constant(SNAME("icon_max_width"));
 
 	theme_cache.font_outline_color = get_theme_color(SNAME("font_outline_color"));
 	theme_cache.font_outline_size = get_theme_constant(SNAME("outline_size"));
@@ -1654,6 +1652,25 @@ void Tree::_update_theme_item_cache() {
 	theme_cache.base_scale = get_theme_default_base_scale();
 }
 
+Size2 Tree::_get_cell_icon_size(const TreeItem::Cell &p_cell) const {
+	Size2i icon_size = p_cell.get_icon_size();
+
+	int max_width = 0;
+	if (theme_cache.icon_max_width > 0) {
+		max_width = theme_cache.icon_max_width;
+	}
+	if (p_cell.icon_max_w > 0 && (max_width == 0 || p_cell.icon_max_w < max_width)) {
+		max_width = p_cell.icon_max_w;
+	}
+
+	if (max_width > 0 && icon_size.width > max_width) {
+		icon_size.height = icon_size.height * max_width / icon_size.width;
+		icon_size.width = max_width;
+	}
+
+	return icon_size;
+}
+
 int Tree::compute_item_height(TreeItem *p_item) const {
 	if ((p_item == root && hide_root) || !p_item->is_visible()) {
 		return 0;
@@ -1688,10 +1705,7 @@ int Tree::compute_item_height(TreeItem *p_item) const {
 			case TreeItem::CELL_MODE_ICON: {
 				Ref<Texture2D> icon = p_item->cells[i].icon;
 				if (!icon.is_null()) {
-					Size2i s = p_item->cells[i].get_icon_size();
-					if (p_item->cells[i].icon_max_w > 0 && s.width > p_item->cells[i].icon_max_w) {
-						s.height = s.height * p_item->cells[i].icon_max_w / s.width;
-					}
+					Size2i s = _get_cell_icon_size(p_item->cells[i]);
 					if (s.height > height) {
 						height = s.height;
 					}
@@ -1745,10 +1759,7 @@ void Tree::draw_item_rect(TreeItem::Cell &p_cell, const Rect2i &p_rect, const Co
 
 	int w = 0;
 	if (!p_cell.icon.is_null()) {
-		Size2i bmsize = p_cell.get_icon_size();
-		if (p_cell.icon_max_w > 0 && bmsize.width > p_cell.icon_max_w) {
-			bmsize.width = p_cell.icon_max_w;
-		}
+		Size2i bmsize = _get_cell_icon_size(p_cell);
 		w += bmsize.width + theme_cache.h_separation;
 		if (rect.size.width > 0 && (w + ts.width) > rect.size.width) {
 			ts.width = rect.size.width - w;
@@ -1788,12 +1799,7 @@ void Tree::draw_item_rect(TreeItem::Cell &p_cell, const Rect2i &p_rect, const Co
 	}
 
 	if (!p_cell.icon.is_null()) {
-		Size2i bmsize = p_cell.get_icon_size();
-
-		if (p_cell.icon_max_w > 0 && bmsize.width > p_cell.icon_max_w) {
-			bmsize.height = bmsize.height * p_cell.icon_max_w / bmsize.width;
-			bmsize.width = p_cell.icon_max_w;
-		}
+		Size2i bmsize = _get_cell_icon_size(p_cell);
 
 		p_cell.draw_icon(ci, rect.position + Size2i(0, Math::floor((real_t)(rect.size.y - bmsize.y) / 2)), bmsize, p_icon_color);
 		rect.position.x += bmsize.x + theme_cache.h_separation;
@@ -2206,12 +2212,7 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2
 					if (p_item->cells[i].icon.is_null()) {
 						break;
 					}
-					Size2i icon_size = p_item->cells[i].get_icon_size();
-					if (p_item->cells[i].icon_max_w > 0 && icon_size.width > p_item->cells[i].icon_max_w) {
-						icon_size.height = icon_size.height * p_item->cells[i].icon_max_w / icon_size.width;
-						icon_size.width = p_item->cells[i].icon_max_w;
-					}
-
+					Size2i icon_size = _get_cell_icon_size(p_item->cells[i]);
 					Point2i icon_ofs = (item_rect.size - icon_size) / 2;
 					icon_ofs += item_rect.position;
 
@@ -3795,8 +3796,9 @@ bool Tree::edit_selected() {
 		popup_rect.size = rect.size;
 
 		// Account for icon.
-		popup_rect.position.x += c.get_icon_size().x;
-		popup_rect.size.x -= c.get_icon_size().x;
+		Size2 icon_size = _get_cell_icon_size(c);
+		popup_rect.position.x += icon_size.x;
+		popup_rect.size.x -= icon_size.x;
 
 		text_editor->clear();
 		text_editor->set_text(c.mode == TreeItem::CELL_MODE_STRING ? c.text : String::num(c.val, Math::range_step_decimals(c.step)));

+ 5 - 1
scene/gui/tree.h

@@ -530,21 +530,24 @@ private:
 		Color font_outline_color;
 
 		float base_scale = 1.0;
+		int font_outline_size = 0;
 
 		int h_separation = 0;
 		int v_separation = 0;
 		int item_margin = 0;
 		int button_margin = 0;
+		int icon_max_width = 0;
 		Point2 offset;
+
 		int draw_relationship_lines = 0;
 		int relationship_line_width = 0;
 		int parent_hl_line_width = 0;
 		int children_hl_line_width = 0;
 		int parent_hl_line_margin = 0;
 		int draw_guides = 0;
+
 		int scroll_border = 0;
 		int scroll_speed = 0;
-		int font_outline_size = 0;
 	} theme_cache;
 
 	struct Cache {
@@ -573,6 +576,7 @@ private:
 	} cache;
 
 	int _get_title_button_height() const;
+	Size2 _get_cell_icon_size(const TreeItem::Cell &p_cell) const;
 
 	void _scroll_moved(float p_value);
 	HScrollBar *h_scroll = nullptr;

+ 5 - 0
scene/resources/default_theme/default_theme.cpp

@@ -180,6 +180,7 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &default_font, const
 	theme->set_color("icon_disabled_color", "Button", Color(1, 1, 1, 0.4));
 
 	theme->set_constant("h_separation", "Button", 2 * scale);
+	theme->set_constant("icon_max_width", "Button", 0);
 
 	// MenuBar
 	theme->set_stylebox("normal", "MenuBar", button_normal);
@@ -688,6 +689,7 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &default_font, const
 	theme->set_constant("separator_outline_size", "PopupMenu", 0);
 	theme->set_constant("item_start_padding", "PopupMenu", 2 * scale);
 	theme->set_constant("item_end_padding", "PopupMenu", 2 * scale);
+	theme->set_constant("icon_max_width", "PopupMenu", 0);
 
 	// GraphNode
 	Ref<StyleBoxFlat> graphnode_normal = make_flat_stylebox(style_normal_color, 18, 42, 18, 12);
@@ -780,6 +782,7 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &default_font, const
 	theme->set_constant("scroll_border", "Tree", 4);
 	theme->set_constant("scroll_speed", "Tree", 12);
 	theme->set_constant("outline_size", "Tree", 0);
+	theme->set_constant("icon_max_width", "Tree", 0);
 
 	// ItemList
 
@@ -842,6 +845,7 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &default_font, const
 
 	theme->set_constant("side_margin", "TabContainer", 8 * scale);
 	theme->set_constant("icon_separation", "TabContainer", 4 * scale);
+	theme->set_constant("icon_max_width", "TabContainer", 0);
 	theme->set_constant("outline_size", "TabContainer", 0);
 
 	// TabBar
@@ -869,6 +873,7 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &default_font, const
 	theme->set_color("drop_mark_color", "TabBar", Color(1, 1, 1));
 
 	theme->set_constant("h_separation", "TabBar", 4 * scale);
+	theme->set_constant("icon_max_width", "TabBar", 0);
 	theme->set_constant("outline_size", "TabBar", 0);
 
 	// Separators