305 lines
7.7 KiB
GDScript
305 lines
7.7 KiB
GDScript
class_name CustomLineEdit
|
|
extends Control
|
|
|
|
# Internal state
|
|
var cursor_pos: int = 0
|
|
var selection: Vector2i = Vector2i(-1, -1)
|
|
var cursor_visible: bool = false
|
|
var has_focus: bool = false
|
|
|
|
# Visual settings
|
|
var cursor_color: Color = Color.BLACK
|
|
|
|
@export var text: String = "":
|
|
set(v):
|
|
text = v.left(max_length)
|
|
cursor_pos = clamp(cursor_pos, 0, text.length())
|
|
queue_redraw()
|
|
|
|
@export_multiline var placeholder: String = "":
|
|
set(v):
|
|
placeholder = v
|
|
queue_redraw()
|
|
|
|
@export_enum("Left", "Center", "Right") var align: int = 0:
|
|
set(v):
|
|
align = v
|
|
queue_redraw()
|
|
|
|
@export_range(0, 1024) var max_length: int = 256
|
|
|
|
@export var secret: bool = false:
|
|
set(v):
|
|
secret = v
|
|
queue_redraw()
|
|
|
|
@export var secret_char: String = "*" :
|
|
set(v):
|
|
secret_char = v[0] if v.length() > 0 else "*"
|
|
queue_redraw()
|
|
|
|
@export var editable: bool = true
|
|
@export var context_menu_enabled: bool = true
|
|
@export var clear_button_enabled: bool = false
|
|
@export var shortcut_keys_enabled: bool = true
|
|
@export var virtual_keyboard_enabled: bool = true
|
|
@export var caret_blink: bool = true
|
|
@export_range(0.1, 2.0) var caret_blink_interval: float = 0.5
|
|
|
|
@export var expand_to_text_length: bool = false
|
|
@export var select_all_on_focus: bool = false
|
|
|
|
@export_group("Colors")
|
|
@export var text_color: Color = Color.BLACK:
|
|
set(v):
|
|
text_color = v
|
|
queue_redraw()
|
|
|
|
@export var placeholder_color: Color = Color.DIM_GRAY:
|
|
set(v):
|
|
placeholder_color = v
|
|
queue_redraw()
|
|
|
|
@export var caret_color: Color = Color.BLACK:
|
|
set(v):
|
|
caret_color = v
|
|
queue_redraw()
|
|
|
|
@export var selection_color: Color = Color(0.7, 0.8, 1.0, 0.5):
|
|
set(v):
|
|
selection_color = v
|
|
queue_redraw()
|
|
|
|
@export_group("Font")
|
|
@export var font: Font = ThemeDB.fallback_font:
|
|
set(v):
|
|
font = v
|
|
queue_redraw()
|
|
|
|
@export_range(8, 64) var font_size: int = 16:
|
|
set(v):
|
|
font_size = v
|
|
queue_redraw()
|
|
|
|
func _ready():
|
|
focus_entered.connect(_on_focus_entered)
|
|
focus_exited.connect(_on_focus_exited)
|
|
set_process_input(true)
|
|
start_blink_timer()
|
|
|
|
func _on_focus_entered():
|
|
has_focus = true
|
|
cursor_visible = true
|
|
queue_redraw()
|
|
|
|
func _on_focus_exited():
|
|
has_focus = false
|
|
cursor_visible = false
|
|
queue_redraw()
|
|
|
|
func start_blink_timer():
|
|
var timer = Timer.new()
|
|
add_child(timer)
|
|
timer.wait_time = 0.5
|
|
timer.timeout.connect(_toggle_cursor_visibility)
|
|
timer.start()
|
|
|
|
func _toggle_cursor_visibility():
|
|
if has_focus:
|
|
cursor_visible = !cursor_visible
|
|
queue_redraw()
|
|
|
|
func _gui_input(event):
|
|
if event is InputEventKey and has_focus:
|
|
handle_key_input(event)
|
|
|
|
if event is InputEventMouseButton and event.pressed:
|
|
if get_rect().has_point(event.position) or true:
|
|
grab_focus()
|
|
handle_mouse_click(event)
|
|
else:
|
|
release_focus()
|
|
|
|
func handle_mouse_click(event: InputEventMouseButton):
|
|
var click_pos = get_local_mouse_position().x
|
|
var text_to_use = get_display_text()
|
|
var offset = get_text_offset(text_to_use)
|
|
|
|
var pos = 0
|
|
var accumulated_width = 0.0
|
|
var found = false
|
|
|
|
for i in range(text_to_use.length()):
|
|
var char_width = font.get_string_size(text_to_use.substr(i, 1), HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).x
|
|
if accumulated_width + char_width/2 > click_pos - offset:
|
|
pos = i
|
|
found = true
|
|
break
|
|
accumulated_width += char_width
|
|
|
|
if not found:
|
|
pos = text_to_use.length()
|
|
|
|
cursor_pos = pos
|
|
selection = Vector2i(-1, -1)
|
|
cursor_visible = true
|
|
queue_redraw()
|
|
|
|
func handle_key_input(event: InputEventKey):
|
|
var shift_pressed = event.shift_pressed
|
|
var ctrl_pressed = event.ctrl_pressed
|
|
|
|
# Handle selection
|
|
if shift_pressed and selection.x == -1:
|
|
selection = Vector2i(cursor_pos, cursor_pos)
|
|
|
|
# Handle text input
|
|
if event.unicode != 0 and !event.echo:
|
|
insert_text_at_cursor(char(event.unicode))
|
|
|
|
# Handle special keys
|
|
match event.keycode:
|
|
KEY_BACKSPACE:
|
|
delete_prev_char()
|
|
KEY_DELETE:
|
|
delete_next_char()
|
|
KEY_LEFT:
|
|
move_cursor(-1, ctrl_pressed, shift_pressed)
|
|
KEY_RIGHT:
|
|
move_cursor(1, ctrl_pressed, shift_pressed)
|
|
KEY_HOME:
|
|
cursor_pos = 0
|
|
adjust_selection(shift_pressed)
|
|
KEY_END:
|
|
cursor_pos = text.length()
|
|
adjust_selection(shift_pressed)
|
|
KEY_C:
|
|
if ctrl_pressed:
|
|
copy_to_clipboard()
|
|
KEY_V:
|
|
if ctrl_pressed:
|
|
paste_from_clipboard()
|
|
|
|
queue_redraw()
|
|
|
|
func insert_text_at_cursor(new_text: String):
|
|
var new_text_filtered = new_text.replace("\n", "").replace("\t", "")
|
|
if text.length() + new_text_filtered.length() > max_length:
|
|
return
|
|
|
|
if selection.x != -1:
|
|
delete_selection()
|
|
|
|
text = text.insert(cursor_pos, new_text_filtered)
|
|
cursor_pos += new_text_filtered.length()
|
|
selection = Vector2i(-1, -1)
|
|
|
|
func delete_prev_char():
|
|
if cursor_pos > 0:
|
|
if selection.x != -1:
|
|
delete_selection()
|
|
else:
|
|
text = text.erase(cursor_pos - 1, 1)
|
|
cursor_pos -= 1
|
|
|
|
func delete_next_char():
|
|
if cursor_pos < text.length():
|
|
if selection.x != -1:
|
|
delete_selection()
|
|
else:
|
|
text = text.erase(cursor_pos, 1)
|
|
|
|
func delete_selection():
|
|
var start = min(selection.x, selection.y)
|
|
var end = max(selection.x, selection.y)
|
|
text = text.erase(start, end - start)
|
|
cursor_pos = start
|
|
selection = Vector2i(-1, -1)
|
|
|
|
func move_cursor(direction: int, ctrl: bool, shift: bool):
|
|
var new_pos = cursor_pos
|
|
if ctrl:
|
|
new_pos = find_word_boundary(direction)
|
|
else:
|
|
new_pos += direction
|
|
|
|
cursor_pos = clamp(new_pos, 0, text.length())
|
|
adjust_selection(shift)
|
|
|
|
func adjust_selection(shift: bool):
|
|
if shift:
|
|
selection.y = cursor_pos
|
|
else:
|
|
selection = Vector2i(-1, -1)
|
|
|
|
func find_word_boundary(direction: int) -> int:
|
|
# Simplified word navigation
|
|
var pos = cursor_pos
|
|
if direction == -1:
|
|
while pos > 0 and text[pos-1] == " ":
|
|
pos -= 1
|
|
while pos > 0 and text[pos-1] != " ":
|
|
pos -= 1
|
|
else:
|
|
while pos < text.length() and text[pos] == " ":
|
|
pos += 1
|
|
while pos < text.length() and text[pos] != " ":
|
|
pos += 1
|
|
return pos
|
|
|
|
func copy_to_clipboard():
|
|
if selection.x != -1:
|
|
var start = min(selection.x, selection.y)
|
|
var end = max(selection.x, selection.y)
|
|
DisplayServer.clipboard_set(text.substr(start, end - start))
|
|
|
|
func paste_from_clipboard():
|
|
var clipboard = DisplayServer.clipboard_get()
|
|
insert_text_at_cursor(clipboard)
|
|
|
|
func get_display_text() -> String:
|
|
if secret and text.length() > 0:
|
|
return secret_char.repeat(text.length())
|
|
return text if text.length() > 0 else placeholder
|
|
|
|
func get_text_offset(display_text: String) -> float:
|
|
var text_width = font.get_string_size(display_text, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).x
|
|
match align:
|
|
HorizontalAlignment.HORIZONTAL_ALIGNMENT_CENTER:
|
|
return max(0, (size.x - text_width) / 2)
|
|
HorizontalAlignment.HORIZONTAL_ALIGNMENT_RIGHT:
|
|
return max(0, size.x - text_width)
|
|
_: # Left align
|
|
return 0.0
|
|
|
|
func _draw():
|
|
var display_text = get_display_text()
|
|
var text_offset = get_text_offset(display_text)
|
|
|
|
# Draw selection
|
|
print(selection)
|
|
if selection.x != -1:
|
|
draw_selection(text_offset, display_text)
|
|
|
|
# Draw text
|
|
var text_color = Color.DIM_GRAY if text == "" else Color.BLACK
|
|
draw_string(font, Vector2(text_offset, size.y / 2 + font_size / 2), display_text,
|
|
HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, text_color)
|
|
|
|
# Draw cursor
|
|
if cursor_visible and has_focus:
|
|
draw_cursor(text_offset, display_text)
|
|
|
|
func draw_selection(offset: float, display_text: String):
|
|
var start = min(selection.x, selection.y)
|
|
var end = max(selection.x, selection.y)
|
|
|
|
var start_pos = offset + font.get_string_size(display_text.left(start), HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).x
|
|
var end_pos = offset + font.get_string_size(display_text.left(end), HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).x
|
|
|
|
draw_rect(Rect2(start_pos, 2, end_pos - start_pos, size.y - 4), selection_color, true)
|
|
|
|
func draw_cursor(offset: float, display_text: String):
|
|
var cursor_x = offset + font.get_string_size(display_text.left(cursor_pos), HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).x
|
|
draw_line(Vector2(cursor_x, 2), Vector2(cursor_x, size.y - 2), cursor_color, 2.0)
|