From 428e5edb8f8a4c9e201ee3a12cd183219623f0c5 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Tue, 19 Nov 2024 01:20:34 +0200 Subject: [PATCH] Add a text tool (#1134) * Initial port of the text tool to Godot 4 * Change font (WIP) * Add antialiasing option and remove some old unneeded lines * Remove outline code * Add horizontal alignment and update the text edit font size * Improve the text edit * Don't activate tools while typing * Format * Give input priority to the text edit so the key X and shortcuts such as control-z work in the text edit * Add style settings for bold and italic * Fix text going blank when changing font * Use `font.draw_multiline_string()` * Change the move behavior of the text tool, add confirm and cancel buttons * Compress images on undo/redo * Fix text position --- assets/graphics/tools/cursors/text.png | Bin 0 -> 218 bytes assets/graphics/tools/cursors/text.png.import | 34 +++ assets/graphics/tools/text.png | Bin 0 -> 246 bytes assets/graphics/tools/text.png.import | 34 +++ project.godot | 10 + src/Autoload/Global.gd | 2 +- src/Autoload/Tools.gd | 9 + src/Main.gd | 2 + src/Tools/UtilityTools/Text.gd | 234 ++++++++++++++++++ src/Tools/UtilityTools/Text.tscn | 139 +++++++++++ src/UI/Nodes/TextToolEdit.gd | 46 ++++ src/UI/ToolsPanel/ToolButtons.gd | 2 +- 12 files changed, 510 insertions(+), 2 deletions(-) create mode 100644 assets/graphics/tools/cursors/text.png create mode 100644 assets/graphics/tools/cursors/text.png.import create mode 100644 assets/graphics/tools/text.png create mode 100644 assets/graphics/tools/text.png.import create mode 100644 src/Tools/UtilityTools/Text.gd create mode 100644 src/Tools/UtilityTools/Text.tscn create mode 100644 src/UI/Nodes/TextToolEdit.gd diff --git a/assets/graphics/tools/cursors/text.png b/assets/graphics/tools/cursors/text.png new file mode 100644 index 0000000000000000000000000000000000000000..db3403d84b730d5cec54e168b079f8c8b8ff5a65 GIT binary patch literal 218 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@H1|*Mc$*~4fjKx9jP7LeL$-D$|rg*wIhFJ7& zo#M@RK!L-h`{m#DKH;WQ8H^oH1tnH|3S4(2B=qgf4iB--e(W!TuN#2%H=F2|MjL*I!Q9Qq8Tkp+dvsDwkHw*i+@@?cd zRXL}&z&0;q&8FYZ6+vv$@@2a|J*?Q6r@5>C!$QH-HC&Os_s#NsrNwSCOR|K>8@%E9 Q2y`EVr>mdKI;Vst07-;WqyPW_ literal 0 HcmV?d00001 diff --git a/assets/graphics/tools/cursors/text.png.import b/assets/graphics/tools/cursors/text.png.import new file mode 100644 index 000000000..b5b877556 --- /dev/null +++ b/assets/graphics/tools/cursors/text.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dn66bu1htli0i" +path="res://.godot/imported/text.png-e400a2b9b6a87e25638acb803c02cdbf.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/graphics/tools/cursors/text.png" +dest_files=["res://.godot/imported/text.png-e400a2b9b6a87e25638acb803c02cdbf.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/graphics/tools/text.png b/assets/graphics/tools/text.png new file mode 100644 index 0000000000000000000000000000000000000000..6a0b504968aa39f997bbdd273d4137ef30542517 GIT binary patch literal 246 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@H1|*Mc$*~4fjKx9jP7LeL$-D$|Hh8)?hFJ7& zo#H5TM1g~)J>=j2)bCA^n-3Y}Xvr{d({<#n PackedStringArray: func find_font_from_name(font_name: String) -> Font: for font in loaded_fonts: if font.get_font_name() == font_name: - return font + return font.duplicate() for system_font_name in OS.get_system_fonts(): if system_font_name == font_name: var system_font := SystemFont.new() diff --git a/src/Autoload/Tools.gd b/src/Autoload/Tools.gd index b236f3d7a..85c7050bc 100644 --- a/src/Autoload/Tools.gd +++ b/src/Autoload/Tools.gd @@ -201,6 +201,15 @@ Hold %s to displace the shape's origin""", ["shape_perfect", "shape_center", "shape_displace"] ) ), + "Text": + Tool.new( + "Text", + "Text", + "text", + "res://src/Tools/UtilityTools/Text.tscn", + [Global.LayerTypes.PIXEL], + "" + ), "3DShapeEdit": Tool.new( "3DShapeEdit", diff --git a/src/Main.gd b/src/Main.gd index 0d1094a41..a933597ae 100644 --- a/src/Main.gd +++ b/src/Main.gd @@ -199,6 +199,8 @@ func _ready() -> void: func _input(event: InputEvent) -> void: + if event is InputEventKey and is_instance_valid(Global.main_viewport): + Global.main_viewport.get_child(0).push_input(event) left_cursor.position = get_global_mouse_position() + Vector2(-32, 32) right_cursor.position = get_global_mouse_position() + Vector2(32, 32) diff --git a/src/Tools/UtilityTools/Text.gd b/src/Tools/UtilityTools/Text.gd new file mode 100644 index 000000000..966f6ea57 --- /dev/null +++ b/src/Tools/UtilityTools/Text.gd @@ -0,0 +1,234 @@ +extends BaseTool + +enum TextStyle { REGULAR, BOLD, ITALIC, BOLD_ITALIC } + +const EMBOLDEN_AMOUNT := 0.6 +const ITALIC_AMOUNT := 0.2 +const ITALIC_TRANSFORM := Transform2D(Vector2(1.0, ITALIC_AMOUNT), Vector2(0.0, 1.0), Vector2.ZERO) + +var text_edit: TextToolEdit: + set(value): + text_edit = value + confirm_buttons.visible = is_instance_valid(text_edit) +var text_size := 16 +var font := FontVariation.new() +var font_name := "": + set(value): + font_name = value + font.base_font = Global.find_font_from_name(font_name) + font.base_font.antialiasing = antialiasing + _textedit_text_changed() +var text_style := TextStyle.REGULAR: + set(value): + text_style = value + match text_style: + TextStyle.REGULAR: + font.variation_embolden = 0 + font.variation_transform = Transform2D() + TextStyle.BOLD: + font.variation_embolden = EMBOLDEN_AMOUNT + font.variation_transform = Transform2D() + TextStyle.ITALIC: + font.variation_embolden = 0 + font.variation_transform = ITALIC_TRANSFORM + TextStyle.BOLD_ITALIC: + font.variation_embolden = EMBOLDEN_AMOUNT + font.variation_transform = ITALIC_TRANSFORM + save_config() + _textedit_text_changed() + +var horizontal_alignment := HORIZONTAL_ALIGNMENT_LEFT +var antialiasing := TextServer.FONT_ANTIALIASING_NONE: + set(value): + antialiasing = value + font.base_font.antialiasing = antialiasing + +var _offset := Vector2i.ZERO + +@onready var confirm_buttons: HBoxContainer = $ConfirmButtons +@onready var font_option_button: OptionButton = $GridContainer/FontOptionButton + + +func _ready() -> void: + var font_names := Global.get_available_font_names() + for f_name in font_names: + font_option_button.add_item(f_name) + Tools.color_changed.connect(_on_color_changed) + super._ready() + + +func get_config() -> Dictionary: + return { + "font_name": font_name, + "text_size": text_size, + "text_style": text_style, + "horizontal_alignment": horizontal_alignment, + "antialiasing": antialiasing + } + + +func set_config(config: Dictionary) -> void: + font_name = config.get("font_name", "Roboto") + if font_name not in Global.get_available_font_names(): + font_name = "Roboto" + text_size = config.get("text_size", text_size) + text_style = config.get("text_style", text_style) + horizontal_alignment = config.get("horizontal_alignment", horizontal_alignment) + antialiasing = config.get("antialiasing", antialiasing) + + +func update_config() -> void: + for i in font_option_button.item_count: + var item_name: String = font_option_button.get_item_text(i) + if font_name == item_name: + font_option_button.selected = i + $TextSizeSlider.value = text_size + + +func draw_start(pos: Vector2i) -> void: + if not is_instance_valid(text_edit): + text_edit = TextToolEdit.new() + text_edit.text = "" + text_edit.font = font + text_edit.add_theme_color_override(&"font_color", tool_slot.color) + text_edit.add_theme_font_size_override(&"font_size", text_size) + Global.canvas.add_child(text_edit) + text_edit.position = pos - Vector2i(0, text_edit.custom_minimum_size.y / 2) + _offset = pos + + +func draw_move(pos: Vector2i) -> void: + if is_instance_valid(text_edit) and not text_edit.get_global_rect().has_point(pos): + text_edit.position += Vector2(pos - _offset) + _offset = pos + + +func draw_end(_position: Vector2i) -> void: + pass + + +func text_to_pixels() -> void: + if not is_instance_valid(text_edit): + return + if text_edit.text.is_empty(): + text_edit.queue_free() + text_edit = null + return + + var undo_data := _get_undo_data() + var project := Global.current_project + var image := project.frames[project.current_frame].cels[project.current_layer].get_image() + + var vp := RenderingServer.viewport_create() + var canvas := RenderingServer.canvas_create() + RenderingServer.viewport_attach_canvas(vp, canvas) + RenderingServer.viewport_set_size(vp, project.size.x, project.size.y) + RenderingServer.viewport_set_disable_3d(vp, true) + RenderingServer.viewport_set_active(vp, true) + RenderingServer.viewport_set_transparent_background(vp, true) + + var ci_rid := RenderingServer.canvas_item_create() + RenderingServer.viewport_set_canvas_transform(vp, canvas, Transform2D()) + RenderingServer.canvas_item_set_parent(ci_rid, canvas) + var texture := RenderingServer.texture_2d_create(image) + RenderingServer.canvas_item_add_texture_rect( + ci_rid, Rect2(Vector2(0, 0), project.size), texture + ) + + var text := text_edit.text + var color := tool_slot.color + var font_ascent := font.get_ascent(text_size) + var pos := Vector2(1, font_ascent + text_edit.get_theme_constant(&"line_spacing")) + pos += text_edit.position + font.draw_multiline_string(ci_rid, pos, text, horizontal_alignment, -1, text_size, -1, color) + + RenderingServer.viewport_set_update_mode(vp, RenderingServer.VIEWPORT_UPDATE_ONCE) + RenderingServer.force_draw(false) + var viewport_texture := RenderingServer.texture_2d_get(RenderingServer.viewport_get_texture(vp)) + RenderingServer.free_rid(vp) + RenderingServer.free_rid(canvas) + RenderingServer.free_rid(ci_rid) + RenderingServer.free_rid(texture) + viewport_texture.convert(Image.FORMAT_RGBA8) + + text_edit.queue_free() + text_edit = null + if not viewport_texture.is_empty(): + image.copy_from(viewport_texture) + commit_undo("Draw", undo_data) + + +func commit_undo(action: String, undo_data: Dictionary) -> void: + var redo_data := _get_undo_data() + var project := Global.current_project + var frame := -1 + var layer := -1 + if Global.animation_timeline.animation_timer.is_stopped() and project.selected_cels.size() == 1: + frame = project.current_frame + layer = project.current_layer + + project.undos += 1 + project.undo_redo.create_action(action) + Global.undo_redo_compress_images(redo_data, undo_data, project) + project.undo_redo.add_do_method(Global.undo_or_redo.bind(false, frame, layer)) + project.undo_redo.add_undo_method(Global.undo_or_redo.bind(true, frame, layer)) + project.undo_redo.commit_action() + + +func _get_undo_data() -> Dictionary: + var data := {} + var images := _get_selected_draw_images() + for image in images: + data[image] = image.data + return data + + +func _on_confirm_button_pressed() -> void: + if is_instance_valid(text_edit): + text_to_pixels() + + +func _on_cancel_button_pressed() -> void: + if is_instance_valid(text_edit): + text_edit.queue_free() + text_edit = null + + +func _textedit_text_changed() -> void: + if not is_instance_valid(text_edit): + return + text_edit.add_theme_font_size_override(&"font_size", 1) # Needed to update font and text style + text_edit.add_theme_font_size_override(&"font_size", text_size) + text_edit._on_text_changed() + + +func _on_color_changed(_color: Color, _button: int) -> void: + if is_instance_valid(text_edit): + text_edit.add_theme_color_override(&"font_color", tool_slot.color) + + +func _on_text_size_slider_value_changed(value: float) -> void: + text_size = value + _textedit_text_changed() + save_config() + + +func _on_font_option_button_item_selected(index: int) -> void: + font_name = font_option_button.get_item_text(index) + save_config() + + +func _on_style_option_button_item_selected(index: TextStyle) -> void: + text_style = index + + +func _on_horizontal_alignment_option_button_item_selected(index: HorizontalAlignment) -> void: + horizontal_alignment = index + + +func _on_antialiasing_option_button_item_selected(index: TextServer.FontAntialiasing) -> void: + antialiasing = index + + +func _exit_tree() -> void: + text_to_pixels() diff --git a/src/Tools/UtilityTools/Text.tscn b/src/Tools/UtilityTools/Text.tscn new file mode 100644 index 000000000..09fb28439 --- /dev/null +++ b/src/Tools/UtilityTools/Text.tscn @@ -0,0 +1,139 @@ +[gd_scene load_steps=6 format=3 uid="uid://ct4o5i1jeul3k"] + +[ext_resource type="PackedScene" uid="uid://ctfgfelg0sho8" path="res://src/Tools/BaseTool.tscn" id="1_1q6ub"] +[ext_resource type="Script" path="res://src/Tools/UtilityTools/Text.gd" id="2_ql5g6"] +[ext_resource type="Texture2D" uid="uid://d267xalp3p7ru" path="res://assets/graphics/misc/check_plain.png" id="3_novww"] +[ext_resource type="Script" path="res://src/UI/Nodes/ValueSlider.gd" id="3_tidsq"] +[ext_resource type="Texture2D" uid="uid://bnc78807k1xjv" path="res://assets/graphics/misc/close.png" id="4_nhcnn"] + +[node name="ToolOptions" instance=ExtResource("1_1q6ub")] +script = ExtResource("2_ql5g6") + +[node name="ConfirmButtons" type="HBoxContainer" parent="." index="2"] +visible = false +layout_mode = 2 + +[node name="ConfirmButton" type="Button" parent="ConfirmButtons" index="0" groups=["UIButtons"]] +custom_minimum_size = Vector2(0, 26) +layout_mode = 2 +size_flags_horizontal = 3 +mouse_default_cursor_shape = 2 + +[node name="TextureRect" type="TextureRect" parent="ConfirmButtons/ConfirmButton" index="0"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +texture = ExtResource("3_novww") +stretch_mode = 3 + +[node name="CancelButton" type="Button" parent="ConfirmButtons" index="1" groups=["UIButtons"]] +custom_minimum_size = Vector2(0, 26) +layout_mode = 2 +size_flags_horizontal = 3 +mouse_default_cursor_shape = 2 + +[node name="TextureRect" type="TextureRect" parent="ConfirmButtons/CancelButton" index="0"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +texture = ExtResource("4_nhcnn") +stretch_mode = 3 + +[node name="TextSizeSlider" type="TextureProgressBar" parent="." index="3"] +custom_minimum_size = Vector2(0, 24) +layout_mode = 2 +focus_mode = 2 +mouse_default_cursor_shape = 2 +theme_type_variation = &"ValueSlider" +min_value = 1.0 +max_value = 128.0 +value = 16.0 +allow_greater = true +nine_patch_stretch = true +stretch_margin_left = 3 +stretch_margin_top = 3 +stretch_margin_right = 3 +stretch_margin_bottom = 3 +script = ExtResource("3_tidsq") +prefix = "Size:" +suffix = "px" + +[node name="GridContainer" type="GridContainer" parent="." index="4"] +layout_mode = 2 +columns = 2 + +[node name="FontLabel" type="Label" parent="GridContainer" index="0"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Font:" + +[node name="FontOptionButton" type="OptionButton" parent="GridContainer" index="1"] +layout_mode = 2 +size_flags_horizontal = 3 +mouse_default_cursor_shape = 2 +fit_to_longest_item = false + +[node name="StyleLabel" type="Label" parent="GridContainer" index="2"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Style:" + +[node name="StyleOptionButton" type="OptionButton" parent="GridContainer" index="3"] +layout_mode = 2 +size_flags_horizontal = 3 +mouse_default_cursor_shape = 2 +selected = 0 +item_count = 4 +popup/item_0/text = "Regular" +popup/item_1/text = "Bold" +popup/item_1/id = 1 +popup/item_2/text = "Italic" +popup/item_2/id = 2 +popup/item_3/text = "Bold Italic" +popup/item_3/id = 3 + +[node name="HorizontalAlignmentLabel" type="Label" parent="GridContainer" index="4"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Horizontal alignment:" + +[node name="HorizontalAlignmentOptionButton" type="OptionButton" parent="GridContainer" index="5"] +layout_mode = 2 +selected = 0 +item_count = 4 +popup/item_0/text = "Left" +popup/item_1/text = "Center" +popup/item_1/id = 1 +popup/item_2/text = "Right" +popup/item_2/id = 2 +popup/item_3/text = "Fill" +popup/item_3/id = 3 + +[node name="AntialiasingLabel" type="Label" parent="GridContainer" index="6"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Antialiasing:" + +[node name="AntialiasingOptionButton" type="OptionButton" parent="GridContainer" index="7"] +layout_mode = 2 +selected = 0 +item_count = 3 +popup/item_0/text = "None" +popup/item_1/text = "Grayscale" +popup/item_1/id = 1 +popup/item_2/text = "LCD" +popup/item_2/id = 2 + +[connection signal="pressed" from="ConfirmButtons/ConfirmButton" to="." method="_on_confirm_button_pressed"] +[connection signal="pressed" from="ConfirmButtons/CancelButton" to="." method="_on_cancel_button_pressed"] +[connection signal="value_changed" from="TextSizeSlider" to="." method="_on_text_size_slider_value_changed"] +[connection signal="item_selected" from="GridContainer/FontOptionButton" to="." method="_on_font_option_button_item_selected"] +[connection signal="item_selected" from="GridContainer/StyleOptionButton" to="." method="_on_style_option_button_item_selected"] +[connection signal="item_selected" from="GridContainer/HorizontalAlignmentOptionButton" to="." method="_on_horizontal_alignment_option_button_item_selected"] +[connection signal="item_selected" from="GridContainer/AntialiasingOptionButton" to="." method="_on_antialiasing_option_button_item_selected"] diff --git a/src/UI/Nodes/TextToolEdit.gd b/src/UI/Nodes/TextToolEdit.gd new file mode 100644 index 000000000..e307a9ae9 --- /dev/null +++ b/src/UI/Nodes/TextToolEdit.gd @@ -0,0 +1,46 @@ +class_name TextToolEdit +extends TextEdit + +var font: Font: + set(value): + font = value + add_theme_font_override(&"font", font) + + +func _ready() -> void: + var stylebox := StyleBoxFlat.new() + stylebox.draw_center = false + stylebox.border_width_left = 1 + stylebox.border_width_top = 1 + stylebox.border_width_right = 1 + stylebox.border_width_bottom = 1 + add_theme_stylebox_override(&"normal", stylebox) + add_theme_stylebox_override(&"focus", stylebox) + add_theme_constant_override(&"line_spacing", 0) + text_changed.connect(_on_text_changed) + theme = Global.control.theme + if is_instance_valid(font): + var font_size := get_theme_font_size(&"font_size") + custom_minimum_size = Vector2(32, maxf(32, font.get_height(font_size))) + size.y = (get_line_count() + 1) * font.get_height(font_size) + + +func _get_max_line() -> int: + var max_line := 0 + var max_string := get_line(0).length() + for i in get_line_count(): + var line := get_line(i) + if line.length() > max_string: + max_string = line.length() + max_line = i + return max_line + + +func _on_text_changed() -> void: + if not is_instance_valid(font): + return + var font_size := get_theme_font_size(&"font_size") + var max_line := get_line(_get_max_line()) + var string_size := font.get_string_size(max_line, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size) + size.x = font_size + string_size.x + size.y = (get_line_count() + 1) * font.get_height(font_size) diff --git a/src/UI/ToolsPanel/ToolButtons.gd b/src/UI/ToolsPanel/ToolButtons.gd index 200e4e631..aee5cbe2c 100644 --- a/src/UI/ToolsPanel/ToolButtons.gd +++ b/src/UI/ToolsPanel/ToolButtons.gd @@ -9,7 +9,7 @@ func _ready() -> void: Global.main_viewport.mouse_exited.connect(set_process_input.bind(false)) -func _input(event: InputEvent) -> void: +func _unhandled_input(event: InputEvent) -> void: if event is InputEventMouseMotion: pen_inverted = event.pen_inverted return