Class: Tuile::Component::TextArea

Inherits:
TextInput 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 Tuile::Component::TextInput#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

Constants inherited from TextInput

Tuile::Component::TextInput::ACTIVE_BG_SGR, Tuile::Component::TextInput::INACTIVE_BG_SGR

Instance Attribute Summary collapse

Attributes inherited from TextInput

#caret, #on_change, #on_escape, #text

Attributes inherited from Tuile::Component

#key_shortcut, #parent, #rect

Instance Method Summary collapse

Methods inherited from TextInput

#empty?, #focusable?, #handle_key, #tab_stop?

Methods inherited from Tuile::Component

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

Constructor Details

#initializeTextArea

Returns a new instance of TextArea.



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

def initialize
  super
  @top_display_row = 0
  # Lazy cache of the word-wrapped layout: an
  # `Array<Hash{Symbol=>Integer}>` whose entries are
  # `{start: <text-index>, length: <chars>}`, one per display row, built
  # by {#compute_display_rows}. `nil` means "stale, recompute on next
  # read". Reset to nil whenever {#text} mutates or the width changes;
  # see {#on_text_mutated} and {#on_width_changed}.
  @display_rows = nil
end

Instance Attribute Details

#top_display_rowInteger (readonly)

Returns index of the topmost display row currently visible.

Returns:

  • (Integer)

    index of the topmost display row currently visible.



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

def top_display_row
  @top_display_row
end

Instance Method Details

#cursor_positionPoint?

Returns:



38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/tuile/component/text_area.rb', line 38

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

#handle_mouse(event) ⇒ void

This method returns an undefined value.

Parameters:



54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/tuile/component/text_area.rb', line 54

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.



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/tuile/component/text_area.rb', line 70

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, Ansi::RESET
  end
end