Class: Tuile::Component::TextArea

Inherits:
Tuile::Component show all
Defined in:
lib/tuile/component/text_area.rb

Overview

A multi-line, word-wrapping text input.

Sized by the caller — #rect is fixed; the area does not grow with content. Text is wrapped to Rect#width columns and any text that doesn’t fit vertically is reached by scrolling: #top_display_row follows the caret so the line being edited stays visible. There is no horizontal scrolling.

The caret is a logical index in ‘0..text.length`. When the caret falls inside a whitespace run that was absorbed by a soft wrap, it displays at the end of the previous row (which is visually identical to the start of the next row in nearly all cases).

Currently only #on_change is wired; Enter inserts a newline as in any plain ‘<textarea>` or text editor. A future `on_enter`/`on_submit` callback may opt out of that by consuming Enter instead.

Constant Summary collapse

ACTIVE_BG_SGR =

Same SGR palette as Tuile::Component::TextField for visual consistency.

Returns:

  • (String)
TextField::ACTIVE_BG_SGR
INACTIVE_BG_SGR =

Returns:

  • (String)
TextField::INACTIVE_BG_SGR
SGR_RESET =

Returns:

  • (String)
TextField::SGR_RESET

Instance Attribute Summary collapse

Attributes inherited from Tuile::Component

#key_shortcut, #parent, #rect

Instance Method Summary collapse

Methods inherited from Tuile::Component

#active=, #active?, #attached?, #children, #content_size, #depth, #find_shortcut_component, #focus, #keyboard_hint, #on_child_removed, #on_focus, #on_tree, #root, #screen

Constructor Details

#initializeTextArea

Returns a new instance of TextArea.



22
23
24
25
26
27
28
29
# File 'lib/tuile/component/text_area.rb', line 22

def initialize
  super
  @text = +""
  @caret = 0
  @top_display_row = 0
  @on_change = nil
  @display_rows = nil
end

Instance Attribute Details

#caretInteger

Returns caret index in ‘0..text.length`.

Returns:

  • (Integer)

    caret index in ‘0..text.length`.



35
36
37
# File 'lib/tuile/component/text_area.rb', line 35

def caret
  @caret
end

#on_changeProc, ...

Optional callback fired whenever #text changes. Receives the new text as a single argument. Not fired by #caret= (text unchanged), not fired by a no-op setter, and not fired by a re-wrap caused by a width change (#text itself is unchanged).

Returns:

  • (Proc, Method, nil)

    one-arg callable, or nil.



45
46
47
# File 'lib/tuile/component/text_area.rb', line 45

def on_change
  @on_change
end

#textString

Returns current text contents (may contain embedded ‘n`).

Returns:

  • (String)

    current text contents (may contain embedded ‘n`).



32
33
34
# File 'lib/tuile/component/text_area.rb', line 32

def text
  @text
end

#top_display_rowInteger (readonly)

Returns index of the topmost display row currently visible.

Returns:

  • (Integer)

    index of the topmost display row currently visible.



38
39
40
# File 'lib/tuile/component/text_area.rb', line 38

def top_display_row
  @top_display_row
end

Instance Method Details

#cursor_positionPoint?

Returns:



79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/tuile/component/text_area.rb', line 79

def cursor_position
  return nil if rect.empty?

  row, col = caret_to_display(@caret)
  screen_row = row - @top_display_row
  return nil if screen_row.negative? || screen_row >= rect.height

  # Cap so the hardware cursor never lands at rect.left+rect.width
  # (one past the rect). Terminals with auto-wrap interpret that as
  # column 0 of the row below; capping pins the cursor on the last
  # visible cell instead.
  Point.new(rect.left + col.clamp(0, rect.width - 1), rect.top + screen_row)
end

#focusable?Boolean

Returns:

  • (Boolean)


74
# File 'lib/tuile/component/text_area.rb', line 74

def focusable? = true

#handle_key(key) ⇒ Boolean

Parameters:

  • key (String)

Returns:

  • (Boolean)


95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/tuile/component/text_area.rb', line 95

def handle_key(key)
  return false unless active?
  return true if super

  case key
  when Keys::LEFT_ARROW then self.caret = @caret - 1
  when Keys::RIGHT_ARROW then self.caret = @caret + 1
  when Keys::CTRL_LEFT_ARROW then self.caret = word_left
  when Keys::CTRL_RIGHT_ARROW then self.caret = word_right
  when Keys::UP_ARROW then move_caret_vertical(-1)
  when Keys::DOWN_ARROW then move_caret_vertical(1)
  when *Keys::HOMES then move_caret_to_row_start
  when *Keys::ENDS_ then move_caret_to_row_end
  when *Keys::BACKSPACES then delete_before_caret
  when Keys::DELETE then delete_at_caret
  when Keys::ENTER then insert_char("\n")
  else
    return insert_char(key) if printable?(key)

    return false
  end
  true
end

#handle_mouse(event) ⇒ void

This method returns an undefined value.

Parameters:



121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/tuile/component/text_area.rb', line 121

def handle_mouse(event)
  super
  return unless event.button == :left && rect.contains?(event.point)

  target_row = (event.y - rect.top) + @top_display_row
  target_col = event.x - rect.left
  rows = display_rows
  if target_row >= rows.size
    self.caret = @text.length
  else
    r = rows[target_row]
    self.caret = r[:start] + target_col.clamp(0, r[:length])
  end
end

#repaintvoid

This method returns an undefined value.



145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/tuile/component/text_area.rb', line 145

def repaint
  return if rect.empty?

  bg = active? ? ACTIVE_BG_SGR : INACTIVE_BG_SGR
  rows = display_rows
  (0...rect.height).each do |screen_row|
    row_idx = screen_row + @top_display_row
    line = if row_idx >= rows.size
             " " * rect.width
           else
             r = rows[row_idx]
             chunk = @text[r[:start], r[:length]] || ""
             chunk + (" " * (rect.width - r[:length]))
           end
    screen.print TTY::Cursor.move_to(rect.left, rect.top + screen_row), bg, line, SGR_RESET
  end
end

#tab_stop?Boolean

Returns:

  • (Boolean)


76
# File 'lib/tuile/component/text_area.rb', line 76

def tab_stop? = true