1
0
Fork 0
mirror of https://github.com/Orama-Interactive/Pixelorama.git synced 2025-01-18 17:19:50 +00:00

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
This commit is contained in:
Emmanouil Papadeas 2024-11-19 01:20:34 +02:00 committed by GitHub
parent f6beb4470e
commit 428e5edb8f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 510 additions and 2 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 B

View file

@ -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

View file

@ -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) "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] [input_devices]

View file

@ -1071,7 +1071,7 @@ func get_available_font_names() -> PackedStringArray:
func find_font_from_name(font_name: String) -> Font: func find_font_from_name(font_name: String) -> Font:
for font in loaded_fonts: for font in loaded_fonts:
if font.get_font_name() == font_name: if font.get_font_name() == font_name:
return font return font.duplicate()
for system_font_name in OS.get_system_fonts(): for system_font_name in OS.get_system_fonts():
if system_font_name == font_name: if system_font_name == font_name:
var system_font := SystemFont.new() var system_font := SystemFont.new()

View file

@ -201,6 +201,15 @@ Hold %s to displace the shape's origin""",
["shape_perfect", "shape_center", "shape_displace"] ["shape_perfect", "shape_center", "shape_displace"]
) )
), ),
"Text":
Tool.new(
"Text",
"Text",
"text",
"res://src/Tools/UtilityTools/Text.tscn",
[Global.LayerTypes.PIXEL],
""
),
"3DShapeEdit": "3DShapeEdit":
Tool.new( Tool.new(
"3DShapeEdit", "3DShapeEdit",

View file

@ -199,6 +199,8 @@ func _ready() -> void:
func _input(event: InputEvent) -> 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) left_cursor.position = get_global_mouse_position() + Vector2(-32, 32)
right_cursor.position = get_global_mouse_position() + Vector2(32, 32) right_cursor.position = get_global_mouse_position() + Vector2(32, 32)

View file

@ -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()

View file

@ -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"]

View file

@ -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)

View file

@ -9,7 +9,7 @@ func _ready() -> void:
Global.main_viewport.mouse_exited.connect(set_process_input.bind(false)) 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: if event is InputEventMouseMotion:
pen_inverted = event.pen_inverted pen_inverted = event.pen_inverted
return return