talkpal-frontend/scenes/common/custom_line_edit.gd
DarkSlein 2707d80723 Redone design
Fixed keyboar on web
2025-03-12 16:41:01 +03:00

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)