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, chrome: BoxChrome::ASCII) ⇒ Canvas

Returns a new instance of Canvas.



26
27
28
29
30
31
32
# File 'lib/tui_tui/canvas.rb', line 26

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

Instance Attribute Details

#chromeObject (readonly)

Returns the value of attribute chrome.



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

def chrome
  @chrome
end

#colsObject (readonly)

Returns the value of attribute cols.



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

def cols
  @cols
end

#cursorObject (readonly)

Returns the value of attribute cursor.



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

def cursor
  @cursor
end

#rowsObject (readonly)

Returns the value of attribute rows.



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

def rows
  @rows
end

Class Method Details

.blank(size, chrome: BoxChrome::ASCII) ⇒ Object



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

def self.blank(size, chrome: BoxChrome::ASCII)
  new(size.rows, size.cols, chrome: chrome)
end

Instance Method Details

#cell(row, col) ⇒ Object



39
40
41
42
43
# File 'lib/tui_tui/canvas.rb', line 39

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.



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

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



34
35
36
37
# File 'lib/tui_tui/canvas.rb', line 34

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

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



84
85
86
87
88
89
90
91
92
# File 'lib/tui_tui/canvas.rb', line 84

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, chrome: @chrome) ⇒ Object



98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/tui_tui/canvas.rb', line 98

def frame(rect, style: FRAME, chrome: @chrome)
  fill(rect, nil)
  mid = chrome.h * (rect.cols - 2)
  text(rect.row, rect.col, chrome.tl + mid + chrome.tr, style)
  text(rect.row + rect.rows - 1, rect.col, chrome.bl + mid + chrome.br, style)
  (1...(rect.rows - 1)).each do |dy|
    text(rect.row + dy, rect.col, chrome.v, style)
    text(rect.row + dy, rect.col + rect.cols - 1, chrome.v, style)
  end

  self
end

#grid_row(r) ⇒ Object



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

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

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



94
95
96
# File 'lib/tui_tui/canvas.rb', line 94

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

#line(row, col, spans) ⇒ Object



74
75
76
77
78
79
80
81
82
# File 'lib/tui_tui/canvas.rb', line 74

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.



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

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)


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

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

#same_size?(other) ⇒ Boolean

Returns:

  • (Boolean)


115
116
117
# File 'lib/tui_tui/canvas.rb', line 115

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

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



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
70
71
72
# File 'lib/tui_tui/canvas.rb', line 45

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