class Fidgit::TextArea

Constants

ENTITIES_AND_TAGS_PATTERN
ENTITY_PLACEHOLDER

Attributes

editable[W]

@param [Boolean] value @return [Boolean]

line_spacing[R]

@return [Number]

max_height[R]

@return [Number]

min_height[R]

@return [Number]

stripped_text[R]

@return [String] Text, but stripped of tags.

Public Class Methods

new(options = {}, &block) click to toggle source

@param (see Element#initialize)

@option (see Element#initialize) @option options [String] :text (“”) @option options [Integer] :height Sets both min and max height at once. @option options [Integer] :min_height @option options [Integer] :max_height (Infinite) @option options [Number] :line_spacing (0) @option options [Boolean] :editable (true)

Calls superclass method Fidgit::Element::new
# File lib/fidgit/elements/text_area.rb, line 125
def initialize(options = {}, &block)
  options = {
    text: '',
    max_height: Float::INFINITY,
    line_spacing: default(:line_spacing),
    background_color: default(:background_color),
    border_color: default(:border_color),
    caret_color: default(:caret_color),
    caret_period: default(:caret_period),
    focused_border_color: default(:focused, :border_color),
    selection_color: default(:selection_color),
    editable: true,
  }.merge! options

  @line_spacing = options[:line_spacing]
  @caret_color = options[:caret_color].dup
  @caret_period = options[:caret_period]
  @focused_border_color = options[:focused_border_color].dup
  @selection_color = options[:selection_color].dup
  @editable = options[:editable]

  @lines = [''] # List of lines of wrapped text.
  @caret_positions = [[0, 0]] # [x, y] of each position the caret can be in.
  @char_widths = [] # Width of each character in the text.
  @text_input = Gosu::TextInput.new
  @old_text = ''
  @old_caret_position = 0
  @old_selection_start = 0
  @tags = Hash.new("") # Hash of tags embedded in the text.

  @text_input.text = options[:text].dup
  @stripped_text = '' # Text stripped of xml tags.

  super(options)

  min_height = padding_left + padding_right + font.height
  if options[:height]
    @max_height = @min_height = [options[:height], min_height].max
  else
    @max_height = [options[:max_height], min_height].max
    @min_height = options[:min_height] ? [options[:min_height], min_height].max : min_height
  end
  rect.height = [padding_left + padding_right + font.height, @min_height].max

  subscribe :left_mouse_button, method(:click_in_text)
  subscribe :right_mouse_button, method(:click_in_text)

  # Handle dragging.
  subscribe :begin_drag do |sender, x, y|
    # Store position of the handle when it starts to drag.
    @drag_start_pos = [x - self.x, y - self.y]
  end

  subscribe :update_drag do |sender, x, y|
    index = text_index_at_position(x, y)
    self.caret_position = [index, @stripped_text.length].min if index
  end

  subscribe :end_drag do
    @drag_start_pos = nil
  end
end

Public Instance Methods

blur(sender) click to toggle source

@return [nil]

# File lib/fidgit/elements/text_area.rb, line 212
def blur(sender)
  if focused?
    $window.current_game_state.focus = nil
    $window.text_input = nil
  end

  @focused = false

  nil
end
caret_position() click to toggle source

Position of the caret.

@return [Integer] Number in range 0..text.length

# File lib/fidgit/elements/text_area.rb, line 89
def caret_position
  @text_input.caret_pos
end
caret_position=(position) click to toggle source

Position of the caret.

@param [Integer] pos Position of caret in the text. @return [Integer] New position of caret.

# File lib/fidgit/elements/text_area.rb, line 97
def caret_position=(position)
  raise ArgumentError, "Caret position must be in the range 0 to the length of the text (inclusive)" unless position.between?(0, stripped_text.length)
  @text_input.caret_pos = position

  position
end
click_in_text(sender, x, y) click to toggle source

@return [nil]

# File lib/fidgit/elements/text_area.rb, line 189
def click_in_text(sender, x, y)
  publish :focus unless focused?

  # Move caret to position the user clicks on.
  index = text_index_at_position x, y
  self.caret_position = @text_input.selection_start = [index, @stripped_text.length].min if index

  nil
end
copy() click to toggle source

Copy the selection to the clipboard.

# File lib/fidgit/elements/text_area.rb, line 454
def copy
  str = selection_text
  Clipboard.copy str unless str.empty?
end
cut() click to toggle source

Cut the selection and copy it to the clipboard.

# File lib/fidgit/elements/text_area.rb, line 444
def cut
  str = selection_text
  unless str.empty?
    Clipboard.copy str
    self.selection_text = '' if editable?
  end
end
drag?(button) click to toggle source
# File lib/fidgit/elements/text_area.rb, line 31
def drag?(button); button == :left; end
draw_foreground() click to toggle source

Draw the text area.

@return [nil]

# File lib/fidgit/elements/text_area.rb, line 226
def draw_foreground
  # Always roll back changes made by the user unless the text is editable.
  if editable? or text == @old_text
    recalc if focused? # Workaround for Windows draw/update bug.
    @old_caret_position = caret_position
    @old_selection_start = @text_input.selection_start
  else
    roll_back
  end

  if caret_position > stripped_text.length
    self.caret_position = stripped_text.length
  end

  if @text_input.selection_start >= stripped_text.length
    @text_input.selection_start = stripped_text.length
  end

  # Draw the selection.
  selection_range.each do |pos|
    char_x, char_y = @caret_positions[pos]
    char_width = @char_widths[pos]
    left, top = x + padding_left + char_x, y + padding_top + char_y
    draw_rect left, top, char_width, font.height, z, @selection_color
  end

  # Draw text.
  @lines.each_with_index do |line, index|
    font.draw(line, x + padding_left, y + padding_top + y_at_line(index), z)
  end

  # Draw the caret.
  if focused? and ((Gosu::milliseconds / @caret_period) % 2 == 0)
    caret_x, caret_y = @caret_positions[caret_position]
    left, top = x + padding_left + caret_x, y + padding_top + caret_y
    draw_rect left, top, 1, font.height, z, @caret_color
  end
end
editable?() click to toggle source

Is the area editable? This will always be false if the Element is disabled.

# File lib/fidgit/elements/text_area.rb, line 34
def editable?
  enabled? and @editable
end
focus(sender) click to toggle source

@return [nil]

# File lib/fidgit/elements/text_area.rb, line 203
def focus(sender)
  @focused = true
  $window.current_game_state.focus = self
  $window.text_input = @text_input

  nil
end
focused?() click to toggle source

Does the element have the focus?

# File lib/fidgit/elements/text_area.rb, line 200
def focused?; @focused; end
paste() click to toggle source

Paste the contents of the clipboard into the TextArea.

# File lib/fidgit/elements/text_area.rb, line 461
def paste
  self.selection_text = Clipboard.paste
end
selection_range() click to toggle source

Returns the range of the selection.

@return [Range]

# File lib/fidgit/elements/text_area.rb, line 47
def selection_range
  from = [@text_input.selection_start, caret_position].min
  to = [@text_input.selection_start, caret_position].max

  (from...to)
end
selection_text() click to toggle source

Returns the text within the selection.

@return [String]

# File lib/fidgit/elements/text_area.rb, line 57
def selection_text
  stripped_text[selection_range]
end
selection_text=(str) click to toggle source

Sets the text within the selection. The caret will be placed at the end of the inserted text.

@param [String] str Text to insert. @return [String] The new selection text.

# File lib/fidgit/elements/text_area.rb, line 65
def selection_text=(str)
  from = [@text_input.selection_start, @text_input.caret_pos].min
  to = [@text_input.selection_start, @text_input.caret_pos].max
  new_length = str.length

  full_text = text
  tags_length_before = (0...from).inject(0) {|m, i| m + @tags[i].length }
  tags_length_inside = (from...to).inject(0) {|m, i| m + @tags[i].length }
  range = (selection_range.first + tags_length_before)...(selection_range.last + tags_length_before + tags_length_inside)
  full_text[range] = str.encode('UTF-8', undef: :replace)
  @text_input.text = full_text

  @text_input.selection_start = @text_input.caret_pos = from + new_length

  recalc # This may roll back the text if it is too long!

  publish :changed, self.text

  str
end
text() click to toggle source

Text within the element. @return [String]

# File lib/fidgit/elements/text_area.rb, line 40
def text
  @text_input.text.force_encoding 'UTF-8'
end
text=(text) click to toggle source

Sets caret to the end of the text.

@param [String] text @return [String] Current string (may be the old one if passed on was too long).

# File lib/fidgit/elements/text_area.rb, line 108
def text=(text)
  @text_input.text = text
  recalc # This may roll back the text if it is too long.
  publish :changed, self.text
  self.text
end

Protected Instance Methods

layout() click to toggle source

@return [nil]

# File lib/fidgit/elements/text_area.rb, line 304
def layout
  # Don't need to re-layout if the text hasn't changed.
  return if @old_text == text

  publish :changed, self.text

  # Save these in case we are too long.
  old_lines = @lines
  old_caret_positions = @caret_positions
  old_char_widths = @char_widths

  @lines = []
  @caret_positions = [[0, 0]] # Position 0 is before the first character.
  @char_widths = []

  space_width = font.text_width ' '
  max_width = width - padding_left - padding_right - space_width

  line = ''
  line_width = 0
  word = ''
  word_width = 0

  strip_tags

  stripped_text.each_char.with_index do |char, i|
    tag = @tags[i]

    # \x0 is just a place-holder for an entity: &entity;
    if char == ENTITY_PLACEHOLDER
      char = tag
      tag = ""
    end

    case char
      when "\n"
        char_width = 0
      else
        char_width = font.text_width char
    end

    overall_width = line_width + (line_width == 0 ? 0 : space_width) + word_width + char_width
    if overall_width > max_width and not (char == ' ' and not word.empty?)
      if line.empty?
        # The current word is longer than the whole word, so split it.
        # Go back and set all the character positions we have.
        position_letters_in_word(word, line_width)

        # Push as much of the current word as possible as a complete line.
        @lines.push word + tag + (char == ' ' ? '' : '-')
        line_width = font.text_width(word)

        word = ''
        word_width = 0
      else

        # Adding the current word would be too wide, so add the current line and start a new one.
        @lines.push line
        line = ''
      end

      widen_last_character line_width
      line_width = 0
    end

    case char
      when "\n"
        # A new-line ends the word and puts it on the line.
        line += word + tag
        line_width = position_letters_in_word(word, line_width)
        @caret_positions.push [line_width, y_at_line(@lines.size)]
        @char_widths.push 0
        widen_last_character line_width
        @lines.push line
        word = ''
        word_width = 0
        line = ''
        line_width = 0

      when ' '
        # A space ends a word and puts it on the line.
        line += word + tag + char
        line_width = position_letters_in_word(word, line_width)
        line_width += space_width
        @caret_positions.push [line_width, y_at_line(@lines.size)]
        @char_widths.push space_width

        word = ''
        word_width = 0

      else
        # If there was a previous line and we start a new line, put the caret pos on the current line.
        if line.empty?
          @caret_positions[-1] = [0, y_at_line(@lines.size)]
        end

        # Start building up a new word.
        word += tag + char
        word_width += char_width
    end
  end

  # Add any remaining word on the last line.
  unless word.empty?
    line_width = position_letters_in_word(word, line_width)
    @char_widths << width - line_width - padding_left - padding_right
    line += word
  end

  @lines.push line if @lines.empty? or not line.empty?

  # Roll back if the height is too long.
  new_height = padding_left + padding_right + y_at_line(@lines.size)
  if new_height <= max_height
    @old_text = text
    rect.height = [new_height, @min_height].max
    @old_caret_position = caret_position
    @old_selection_start = @text_input.selection_start
  else
    roll_back
  end

  nil
end
position_letters_in_word(word, line_width) click to toggle source

Helper for recalc @return [Integer]

# File lib/fidgit/elements/text_area.rb, line 290
def position_letters_in_word(word, line_width)
  # Strip tags before measuring word.
  word.gsub(ENTITIES_AND_TAGS_PATTERN, '').each_char do |c|
    char_width = font.text_width(c)
    line_width += char_width
    @caret_positions.push [line_width, y_at_line(@lines.size)]
    @char_widths.push char_width
  end

  line_width
end
post_init_block(&block) click to toggle source

Use block as an event handler.

# File lib/fidgit/elements/text_area.rb, line 467
def post_init_block(&block)
  subscribe :changed, &block
end
roll_back() click to toggle source
# File lib/fidgit/elements/text_area.rb, line 430
def roll_back
  @text_input.text = @old_text
  self.caret_position = @old_caret_position
  @text_input.selection_start = @old_selection_start
  recalc
end
strip_tags() click to toggle source

Strip XML tags and entities (“<c=000000></c>” and “&entity;”) @note Entities will mess up the system because we don't know how wide they are.

# File lib/fidgit/elements/text_area.rb, line 474
def strip_tags
  tags_length = 0
  @tags = Hash.new('')

  @stripped_text = text.gsub(ENTITIES_AND_TAGS_PATTERN) do |tag|
    pos = $`.length - tags_length
    tags_length += tag.length
    @tags[pos] += tag

    # Entities need to have a non-printing character that can represent them.
    # Still not right, but does mean there are the right number of characters.
    if tag[0] == '&'
      tags_length -= 1
      ENTITY_PLACEHOLDER # Will be expanded later.
    else
      '' # Tags don't use up space, so ignore them.
    end
  end
end
text_index_at_position(x, y) click to toggle source

Index of character in reference to the displayable text.

# File lib/fidgit/elements/text_area.rb, line 267
def text_index_at_position(x, y)
  # Move caret to position the user clicks on.
  mouse_x, mouse_y = x - (self.x + padding_left), y - (self.y + padding_top)
  @char_widths.each.with_index do |width, i|
    char_x, char_y = @caret_positions[i]
    if mouse_x.between?(char_x, char_x + width) and mouse_y.between?(char_y, char_y + font.height)
      return i
    end
  end

  nil # Didn't find a character at that position.
end
widen_last_character(line_width) click to toggle source
# File lib/fidgit/elements/text_area.rb, line 438
def widen_last_character(line_width)
  @char_widths[-1] += (width - line_width - padding_left - padding_right) unless @char_widths.empty?
end
y_at_line(lines_number) click to toggle source

y position of the

# File lib/fidgit/elements/text_area.rb, line 282
def y_at_line(lines_number)
  lines_number * (font.height + line_spacing)
end