diff --git a/assets/graphics/tools/cursors/text.png b/assets/graphics/tools/cursors/text.png new file mode 100644 index 000000000..db3403d84 Binary files /dev/null and b/assets/graphics/tools/cursors/text.png differ 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 000000000..6a0b50496 Binary files /dev/null and b/assets/graphics/tools/text.png differ diff --git a/assets/graphics/tools/text.png.import b/assets/graphics/tools/text.png.import new file mode 100644 index 000000000..a03c94553 --- /dev/null +++ b/assets/graphics/tools/text.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bwpucajlx84xt" +path="res://.godot/imported/text.png-627e4dab52ac32f8208bc01b5803fe72.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/graphics/tools/text.png" +dest_files=["res://.godot/imported/text.png-627e4dab52ac32f8208bc01b5803fe72.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/project.godot b/project.godot index aa7ddb82d..4e8a3fed5 100644 --- a/project.godot +++ b/project.godot @@ -911,6 +911,16 @@ center_canvas={ "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":67,"physical_keycode":0,"key_label":0,"unicode":67,"location":0,"echo":false,"script":null) ] } +left_text_tool={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null) +] +} +right_text_tool={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":true,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null) +] +} [input_devices] diff --git a/src/Autoload/Global.gd b/src/Autoload/Global.gd index 21a04d459..d76ed92a6 100644 --- a/src/Autoload/Global.gd +++ b/src/Autoload/Global.gd @@ -1071,7 +1071,7 @@ func get_available_font_names() -> 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