Files
infinitefusion-e18/Data/Scripts/801_UI controls/Control elements/004_TextBox.rb

313 lines
10 KiB
Ruby

#===============================================================================
# TODO: Support selecting part of the text by remembering the initial
# cursor position and using it and the current cursor position to
# decide which characters are selected. Maybe? Note that this method
# is only triggered upon the initial mouse press, and isn't repeated
# while it's still held down.
#===============================================================================
class UIControls::TextBox < UIControls::BaseControl
TEXT_BOX_X = 2
TEXT_BOX_WIDTH = 172
TEXT_BOX_HEIGHT = 24
TEXT_BOX_PADDING = 4 # Gap between sides of text box and text
TEXT_OFFSET_Y = 5
def initialize(width, height, viewport, value = "")
super(width, height, viewport)
@value = value
@cursor_pos = -1
@display_pos = 0
@cursor_timer = nil
@cursor_shown = false
@blacklist = []
end
def value=(new_value)
return if @value == new_value
@value = new_value
invalidate
end
def insert_char(ch)
@value.insert(@cursor_pos, ch)
@cursor_pos += 1
@cursor_timer = System.uptime
@cursor_shown = true
invalidate
end
def delete_at(index)
@value.slice!(index)
@cursor_pos -= 1 if @cursor_pos > index
@cursor_timer = System.uptime
@cursor_shown = true
invalidate
end
def cursor_pos=(val)
@cursor_pos = val
reset_display_pos
@cursor_timer = System.uptime
@cursor_shown = true
invalidate
end
def set_blacklist(*list)
@blacklist = list
invalidate
end
def set_interactive_rects
@text_box_rect = Rect.new(TEXT_BOX_X, (height - TEXT_BOX_HEIGHT) / 2,
[TEXT_BOX_WIDTH, width].min, TEXT_BOX_HEIGHT)
@interactions = {
:text_box => @text_box_rect
}
end
#-----------------------------------------------------------------------------
def disabled?
val = (@value.respond_to?("strip!")) ? @value.strip : @value
return true if @blacklist.include?(val)
return super
end
def busy?
return @cursor_pos >= 0 if @captured_area == :text_box
return super
end
def reset_interaction
@cursor_pos = -1
@display_pos = 0
@cursor_timer = nil
@initial_value = nil
Input.text_input = false
invalidate
end
#-----------------------------------------------------------------------------
def get_cursor_index_from_mouse_position
char_widths = []
@value.to_s.length.times { |i| char_widths[i] = self.bitmap.text_size(@value.to_s[i]).width }
mouse_x, mouse_y = mouse_pos
mouse_x -= @text_box_rect.x + TEXT_BOX_PADDING
return 0 if mouse_x < 0
(@display_pos...char_widths.length).each do |i|
mouse_x -= char_widths[i]
if mouse_x <= 0
return (mouse_x.abs >= char_widths[i] / 2) ? i : i + 1
end
end
return @value.to_s.length
end
def reset_display_pos
box_width = @text_box_rect.width - (TEXT_BOX_PADDING * 2)
char_widths = []
@value.to_s.length.times { |i| char_widths[i] = self.bitmap.text_size(@value.to_s[i]).width }
# Text isn't wider than the box
if char_widths.sum <= box_width
return false if @display_pos == 0
@display_pos = 0
return true
end
display_pos_changed = false
# Ensure the cursor hasn't gone off the left side of the text box
if @cursor_pos < @display_pos
@display_pos = @cursor_pos
display_pos_changed = true
end
# Ensure the cursor hasn't gone off the right side of the text box
if @cursor_pos > @display_pos
loop do
cursor_x = 0
(@display_pos...@cursor_pos).each do |i|
cursor_x += char_widths[i] if char_widths[i]
end
break if cursor_x < box_width
@display_pos += 1
display_pos_changed = true
break if @display_pos == @cursor_pos
end
end
# Ensure there isn't empty space on the right if the text can be moved to
# the right to fill it
if @display_pos > 0
cursor_x = 0
(@display_pos...char_widths.length).each do |i|
cursor_x += char_widths[i] if char_widths[i]
end
loop do
cursor_x += char_widths[@display_pos - 1]
break if cursor_x >= box_width
@display_pos -= 1
display_pos_changed = true
break if @display_pos == 0
end
end
return display_pos_changed
end
#-----------------------------------------------------------------------------
def draw_area_highlight
return if @captured_area == :text_box && (@hover_area == @captured_area || !Input.press?(Input::MOUSELEFT))
super
end
def draw_cursor(cursor_x)
return if !@cursor_shown || @cursor_pos < 0
cursor_y_offset = ((height - TEXT_BOX_HEIGHT) / 2) + 2
cursor_height = height - (cursor_y_offset * 2)
bitmap.fill_rect(cursor_x, cursor_y_offset, 2, cursor_height, self.bitmap.font.color)
end
def refresh
super
# Draw disabled colour
if disabled?
self.bitmap.fill_rect(@text_box_rect.x, @text_box_rect.y,
@text_box_rect.width, @text_box_rect.height,
DISABLED_COLOR)
end
# Draw text box outline
self.bitmap.outline_rect(@text_box_rect.x, @text_box_rect.y,
@text_box_rect.width, @text_box_rect.height,
self.bitmap.font.color)
# Draw value
char_x = @text_box_rect.x + TEXT_BOX_PADDING
last_char_index = @display_pos
(@value.to_s.length - @display_pos).times do |i|
char = @value.to_s[@display_pos + i]
char_width = self.bitmap.text_size(char).width
cannot_display_next_char = char_x + char_width > @text_box_rect.x + @text_box_rect.width - TEXT_BOX_PADDING
draw_text(self.bitmap, char_x, TEXT_OFFSET_Y, char) if !cannot_display_next_char
# Draw cursor
draw_cursor(char_x - 1) if @display_pos + i == @cursor_pos
break if cannot_display_next_char
last_char_index = @display_pos + i
char_x += char_width
end
# Draw cursor at end
draw_cursor(char_x - 1) if @cursor_pos == @value.to_s.length
# Draw left/right arrows to indicate more text beyond the text box sides
if @display_pos > 0
bitmap.fill_rect(@text_box_rect.x, (height / 2) - 4, 1, 8, Color.white)
5.times do |i|
bitmap.fill_rect(@text_box_rect.x - 2 + i, (height / 2) - (i + 1), 1, 2 * (i + 1), self.bitmap.font.color)
end
end
if last_char_index < @value.to_s.length - 1
bitmap.fill_rect(@text_box_rect.x + @text_box_rect.width - 1, (height / 2) - 4, 1, 8, Color.white)
5.times do |i|
bitmap.fill_rect(@text_box_rect.x + @text_box_rect.width + 1 - i, (height / 2) - (i + 1), 1, 2 * (i + 1), self.bitmap.font.color)
end
end
end
#-----------------------------------------------------------------------------
def on_mouse_press
@captured_area = nil
super
if @captured_area == :text_box
# Clicked into the text box; put the text cursor in there
@cursor_pos = get_cursor_index_from_mouse_position
@cursor_timer = System.uptime
invalidate
else
@value.strip! if @value.respond_to?("strip!")
@value = @initial_value if disabled?
set_changed if @initial_value && @value != @initial_value
reset_interaction
end
end
def on_mouse_release
return if !@captured_area # Wasn't captured to begin with
# Start text entry if clicked and released mouse button in the text box
if @captured_area == :text_box
mouse_x, mouse_y = mouse_pos
if mouse_x && mouse_y && @interactions[@captured_area].contains?(mouse_x, mouse_y)
@initial_value = @value.clone
Input.text_input = true
invalidate
return # This control is still captured
end
end
# Released mouse button outside of text box, or initially clicked outside of
# text box; end interaction with this control
@value.strip! if @value.respond_to?("strip!")
@value = @initial_value if disabled?
set_changed if @initial_value && @value != @initial_value
reset_interaction
super # Make this control not busy again
end
def update_special_inputs
# Left/right to move cursor
if Input.triggerex?(:LEFT) || Input.repeatex?(:LEFT)
self.cursor_pos = @cursor_pos - 1 if @cursor_pos > 0
elsif Input.triggerex?(:RIGHT) || Input.repeatex?(:RIGHT)
self.cursor_pos = @cursor_pos + 1 if @cursor_pos < @value.to_s.length
end
# Home/End to jump to start/end of the text
if Input.triggerex?(:HOME) || Input.repeatex?(:HOME)
self.cursor_pos = 0
elsif Input.triggerex?(:END) || Input.repeatex?(:END)
self.cursor_pos = @value.to_s.length
end
# Backspace/Delete to remove text
if Input.triggerex?(:BACKSPACE) || Input.repeatex?(:BACKSPACE)
delete_at(@cursor_pos - 1) if @cursor_pos > 0
elsif Input.triggerex?(:DELETE) || Input.repeatex?(:DELETE)
delete_at(@cursor_pos) if @cursor_pos < @value.to_s.length
end
# Return/Escape to end text input (Escape undoes the change)
if Input.triggerex?(:RETURN) || Input.repeatex?(:RETURN) ||
Input.triggerex?(:KP_ENTER) || Input.repeatex?(:KP_ENTER)
@value.strip! if @value.respond_to?("strip!")
@value = @initial_value if disabled?
set_changed if @initial_value && @value != @initial_value
reset_interaction
@captured_area = nil
elsif Input.triggerex?(:ESCAPE) || Input.repeatex?(:ESCAPE)
@value = @initial_value if @initial_value
reset_interaction
@captured_area = nil
end
end
def update_text_entry
ret = false
Input.gets.each_char do |ch|
insert_char(ch)
ret = true
end
return ret
end
def update
return if !self.visible
super
# Make the cursor flash
if @captured_area == :text_box
cursor_to_show = ((System.uptime - @cursor_timer) / 0.35).to_i.even?
if cursor_to_show != @cursor_shown
@cursor_shown = cursor_to_show
invalidate
end
old_cursor_pos = @cursor_pos
# Update cursor movement, deletions and ending text input
update_special_inputs
return if @cursor_pos != old_cursor_pos || !busy?
# Detect character input and add them to @value
char_inserted = update_text_entry
invalidate if reset_display_pos || char_inserted
end
end
end