Class: TuiTui::Canvas

Inherits:
Object
  • Object
show all
Defined in:
lib/tui_tui/canvas.rb

Overview

Pure drawing surface. Coordinates are 1-origin to match terminal cursor addressing, and text layout is terminal-column aware.

Constant Summary collapse

CONTROL_GLYPH =

Control bytes are rendered visibly instead of being emitted to the terminal.

"?"
FRAME =
Style.new(fg: :bright_black)

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(rows, cols) ⇒ Canvas

Returns a new instance of Canvas.



24
25
26
27
28
29
# File 'lib/tui_tui/canvas.rb', line 24

def initialize(rows, cols)
  @rows = rows
  @cols = cols
  @grid = Array.new(rows) { Array.new(cols, Cell::BLANK) }
  @cursor = nil
end

Instance Attribute Details

#colsObject (readonly)

Returns the value of attribute cols.



21
22
23
# File 'lib/tui_tui/canvas.rb', line 21

def cols
  @cols
end

#cursorObject (readonly)

Returns the value of attribute cursor.



22
23
24
# File 'lib/tui_tui/canvas.rb', line 22

def cursor
  @cursor
end

#rowsObject (readonly)

Returns the value of attribute rows.



21
22
23
# File 'lib/tui_tui/canvas.rb', line 21

def rows
  @rows
end

Class Method Details

.blank(size) ⇒ Object



17
18
19
# File 'lib/tui_tui/canvas.rb', line 17

def self.blank(size)
  new(size.rows, size.cols)
end

Instance Method Details

#cell(row, col) ⇒ Object



36
37
38
39
40
# File 'lib/tui_tui/canvas.rb', line 36

def cell(row, col)
  return nil unless row.between?(1, @rows) && col.between?(1, @cols)

  @grid[row - 1][col - 1]
end

#changed_span(other, r) ⇒ Object

The changed column span of row ‘r` versus `other`, as [from, to] (1-origin, inclusive), or nil if the row is identical. The start is backed up off any wide-char continuation cell so a partial repaint never begins mid-glyph. Used by the compositor to repaint only the part of a row that moved.



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/tui_tui/canvas.rb', line 120

def changed_span(other, r)
  mine = grid_row(r)
  theirs = other.grid_row(r)
  first = last = nil
  mine.each_index do |i|
    next if mine[i] == theirs[i]

    first ||= i
    last = i
  end

  return nil if first.nil?

  first -= 1 while first.positive? && mine[first].continuation?
  [first + 1, last + 1]
end

#cursor_at(row, col) ⇒ Object



31
32
33
34
# File 'lib/tui_tui/canvas.rb', line 31

def cursor_at(row, col)
  @cursor = [row, col] if row.between?(1, @rows) && col.between?(1, @cols)
  self
end

#fill(rect, style, char = " ") ⇒ Object



81
82
83
84
85
86
87
88
89
# File 'lib/tui_tui/canvas.rb', line 81

def fill(rect, style, char = " ")
  cell = Cell.new(char: fill_char(char), style: style)
  rect.rows.times do |dr|
    row = rect.row + dr
    rect.cols.times { |dc| place(row, rect.col + dc, cell) }
  end

  self
end

#frame(rect, style: FRAME) ⇒ Object



95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/tui_tui/canvas.rb', line 95

def frame(rect, style: FRAME)
  fill(rect, nil)
  bar = "+#{"-" * (rect.cols - 2)}+"
  text(rect.row, rect.col, bar, style)
  text(rect.row + rect.rows - 1, rect.col, bar, style)
  (1...(rect.rows - 1)).each do |dy|
    text(rect.row + dy, rect.col, "|", style)
    text(rect.row + dy, rect.col + rect.cols - 1, "|", style)
  end

  self
end

#grid_row(r) ⇒ Object



159
# File 'lib/tui_tui/canvas.rb', line 159

def grid_row(r) = @grid[r - 1]

#hline(row, col, len, char = "-", style = nil) ⇒ Object



91
92
93
# File 'lib/tui_tui/canvas.rb', line 91

def hline(row, col, len, char = "-", style = nil)
  text(row, col, char * len, style)
end

#line(row, col, spans) ⇒ Object



71
72
73
74
75
76
77
78
79
# File 'lib/tui_tui/canvas.rb', line 71

def line(row, col, spans)
  column = col
  spans.each do |span|
    text(row, column, span.text, span.style)
    column += DisplayText.new(span.text).width
  end

  self
end

#render_row(r, from: 1, to: @cols, depth: :ansi256, enabled: true) ⇒ Object

Render row ‘r`, or just the column span [from, to], coalescing same-styled runs and skipping wide-char continuation cells.



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/tui_tui/canvas.rb', line 139

def render_row(r, from: 1, to: @cols, depth: :ansi256, enabled: true)
  out = +""
  run = +""
  run_style = :none
  grid_row(r)[(from - 1)..(to - 1)].each do |c|
    next if c.continuation?

    if run_style != :none && run_style != c.style
      out << paint(run, run_style, depth, enabled)
      run = +""
    end

    run_style = c.style
    run << c.char
  end

  out << paint(run, run_style, depth, enabled) unless run.empty?
  out
end

#same_row?(other, r) ⇒ Boolean

Returns:

  • (Boolean)


108
109
110
# File 'lib/tui_tui/canvas.rb', line 108

def same_row?(other, r)
  grid_row(r) == other.grid_row(r)
end

#same_size?(other) ⇒ Boolean

Returns:

  • (Boolean)


112
113
114
# File 'lib/tui_tui/canvas.rb', line 112

def same_size?(other)
  @rows == other.rows && @cols == other.cols
end

#text(row, col, string, style = nil) ⇒ Object



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/tui_tui/canvas.rb', line 42

def text(row, col, string, style = nil)
  return self unless row.between?(1, @rows)

  column = col
  TextSanitizer.sanitize(string.to_s).each_grapheme_cluster do |grapheme|
    if Width.control?(grapheme.ord)
      break if column > @cols

      place(row, column, Cell.new(char: CONTROL_GLYPH, style: style))
      column += 1
      next
    end

    width = Width.cluster(grapheme)
    # Leading combining marks have no base cell to attach to.
    next if width.zero?

    break if column > @cols
    # Do not split a wide glyph across the right edge.
    break if width == 2 && column == @cols

    place(row, column, Cell.new(char: grapheme, style: style))
    place(row, column + 1, Cell.new(char: nil, style: style)) if width == 2
    column += width
  end

  self
end