Class: Tuile::Component::TextArea
- Inherits:
-
TextInput
- Object
- Tuile::Component
- TextInput
- Tuile::Component::TextArea
- 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
-
#top_display_row ⇒ Integer
readonly
Index of the topmost display row currently visible.
Attributes inherited from TextInput
#caret, #on_change, #on_escape, #text
Attributes inherited from Tuile::Component
Instance Method Summary collapse
- #cursor_position ⇒ Point?
- #handle_mouse(event) ⇒ void
-
#initialize ⇒ TextArea
constructor
A new instance of TextArea.
- #repaint ⇒ void
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
#initialize ⇒ TextArea
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_row ⇒ Integer (readonly)
Returns 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_position ⇒ Point?
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.
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. == :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 |
#repaint ⇒ void
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 |