Class: Clogs::Paragraph
- Inherits:
-
Object
- Object
- Clogs::Paragraph
- Defined in:
- lib/clogs/paragraph.rb
Overview
Word-level text layout.
libui will happily wrap a whole attributed string for us, but it will not
tell us where it put anything -- there is no hit-testing or caret API on
uiDrawTextLayout. Shoes needs that geometry: links have to be clickable,
para#hit has to map a pixel to a character, and the editor has to draw a
caret.
So Clogs does its own line breaking. Each run is split into words, each word is measured once and cached, and lines are filled greedily. Drawing then batches adjacent words that share a style back into a single layout, so the common case still costs one libui text layout per line per style.
Defined Under Namespace
Constant Summary collapse
- SIZES =
Shoes' named sizes, matching Scarpe's.
{ inscription: 10, ins: 10, para: 12, caption: 14, tagline: 18, subtitle: 26, title: 34, banner: 48 }.freeze
Instance Attribute Summary collapse
-
#height ⇒ Object
readonly
Returns the value of attribute height.
-
#lines ⇒ Object
readonly
Returns the value of attribute lines.
-
#placed ⇒ Object
readonly
Returns the value of attribute placed.
-
#width ⇒ Object
readonly
Returns the value of attribute width.
Class Method Summary collapse
- .line_height(style) ⇒ Object
- .measure(text, style) ⇒ Object
-
.measure_cache ⇒ Object
Cache of [style_key, text] => [width, height].
-
.tokenize(text) ⇒ Object
Split into words while keeping the whitespace attached to the preceding word, so that trailing spaces do not start a new line on their own.
Instance Method Summary collapse
- #align_line(from, line_width, _line_height) ⇒ Object
-
#boxes_for(owner) ⇒ Object
Bounding boxes for everything contributed by
owner, used to make links clickable. -
#draw(painter, ox, oy) ⇒ Object
Draw, batching runs that share a line and a style.
-
#index_at(px, py) ⇒ Object
Character index nearest to a point, for text selection.
-
#initialize(runs, wrap_width, align: :left) ⇒ Paragraph
constructor
A new instance of Paragraph.
- #layout ⇒ Object
Constructor Details
#initialize(runs, wrap_width, align: :left) ⇒ Paragraph
Returns a new instance of Paragraph.
84 85 86 87 88 89 |
# File 'lib/clogs/paragraph.rb', line 84 def initialize(runs, wrap_width, align: :left) @runs = runs @wrap_width = wrap_width.to_f @align = align layout end |
Instance Attribute Details
#height ⇒ Object (readonly)
Returns the value of attribute height.
27 28 29 |
# File 'lib/clogs/paragraph.rb', line 27 def height @height end |
#lines ⇒ Object (readonly)
Returns the value of attribute lines.
27 28 29 |
# File 'lib/clogs/paragraph.rb', line 27 def lines @lines end |
#placed ⇒ Object (readonly)
Returns the value of attribute placed.
27 28 29 |
# File 'lib/clogs/paragraph.rb', line 27 def placed @placed end |
#width ⇒ Object (readonly)
Returns the value of attribute width.
27 28 29 |
# File 'lib/clogs/paragraph.rb', line 27 def width @width end |
Class Method Details
.line_height(style) ⇒ Object
55 56 57 58 59 60 61 62 63 |
# File 'lib/clogs/paragraph.rb', line 55 def line_height(style) key = [style.cache_key, :__line_height].freeze measure_cache[key] ||= begin w, h = measure("Hg", style) _ = w [0.0, h] end measure_cache[key][1] end |
.measure(text, style) ⇒ Object
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
# File 'lib/clogs/paragraph.rb', line 37 def measure(text, style) return [0.0, line_height(style)] if text.empty? key = [style.cache_key, text].freeze measure_cache[key] ||= begin block = TextBlock.new( [Run.new(text: text, size: style.size, family: style.family, bold: style.bold, italic: style.italic, color: style.color)], -1, default_family: style.family, default_size: style.size ) dims = [block.width, block.height] block.free dims end end |
.measure_cache ⇒ Object
Cache of [style_key, text] => [width, height]. Text-heavy apps repeat the same words constantly, and each miss costs an attributed string, a text layout and two FFI calls.
33 34 35 |
# File 'lib/clogs/paragraph.rb', line 33 def measure_cache @measure_cache ||= {} end |
.tokenize(text) ⇒ Object
Split into words while keeping the whitespace attached to the preceding word, so that trailing spaces do not start a new line on their own.
93 94 95 |
# File 'lib/clogs/paragraph.rb', line 93 def self.tokenize(text) text.scan(/\n|[^\s]+[ \t]*|[ \t]+/) end |
Instance Method Details
#align_line(from, line_width, _line_height) ⇒ Object
145 146 147 148 149 150 151 152 153 154 155 156 |
# File 'lib/clogs/paragraph.rb', line 145 def align_line(from, line_width, _line_height) return if @align == :left || @wrap_width <= 0 shift = case @align when :center then (@wrap_width - line_width) / 2.0 when :right then @wrap_width - line_width else 0.0 end return if shift <= 0 (from...@placed.size).each { |i| @placed[i].x += shift } end |
#boxes_for(owner) ⇒ Object
Bounding boxes for everything contributed by owner, used to make links
clickable. One box per line the owner appears on.
197 198 199 200 201 202 203 204 205 |
# File 'lib/clogs/paragraph.rb', line 197 def boxes_for(owner) items = @placed.select { |i| i.owner.equal?(owner) } items.group_by(&:y).map do |y, line_items| x0 = line_items.map(&:x).min x1 = line_items.map { |i| i.x + i.width }.max h = line_items.map(&:height).max [x0, y, x1 - x0, h] end end |
#draw(painter, ox, oy) ⇒ Object
Draw, batching runs that share a line and a style.
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 |
# File 'lib/clogs/paragraph.rb', line 159 def draw(painter, ox, oy) batch = [] flush = lambda do next if batch.empty? first = batch.first text = batch.map(&:text).join style = first.style block = TextBlock.new( [Run.new(text: text, size: style.size, family: style.family, bold: style.bold, italic: style.italic, underline: style.underline, color: style.color)], -1, default_family: style.family, default_size: style.size ) if style.bg h = batch.map(&:height).max w = batch.sum(&:width) painter.fill_rect(ox + first.x, oy + first.y, w, h, style.bg) end block.draw(painter, ox + first.x, oy + first.y) block.free batch = [] end @placed.each do |item| prev = batch.last if prev && (prev.y != item.y || !prev.style.same_run_as?(item.style) || prev.style.bg != item.style.bg) flush.call end batch << item end flush.call end |
#index_at(px, py) ⇒ Object
Character index nearest to a point, for text selection.
208 209 210 211 212 213 214 215 216 |
# File 'lib/clogs/paragraph.rb', line 208 def index_at(px, py) line = @placed.select { |i| py >= i.y && py < i.y + i.height } line = @placed if line.empty? return 0 if line.empty? item = line.find { |i| px < i.x + i.width } || line.last frac = item.width.zero? ? 0 : (px - item.x) / item.width item.char_offset + (frac * item.text.length).round.clamp(0, item.text.length) end |
#layout ⇒ Object
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
# File 'lib/clogs/paragraph.rb', line 97 def layout @placed = [] x = 0.0 y = 0.0 line_start = 0 line_height = 0.0 char_offset = 0 max_x = 0.0 flush_line = lambda do align_line(line_start, x, line_height) max_x = [max_x, x].max y += line_height.positive? ? line_height : 0 x = 0.0 line_height = 0.0 line_start = @placed.size end @runs.each do |text, style| self.class.tokenize(text.to_s).each do |token| if token == "\n" line_height = self.class.line_height(style) if line_height.zero? char_offset += 1 flush_line.call next end w, h = self.class.measure(token, style) # Trailing spaces should not force a wrap. stripped = token.rstrip effective_w = stripped == token ? w : self.class.measure(stripped, style)[0] if x.positive? && @wrap_width.positive? && x + effective_w > @wrap_width flush_line.call end @placed << Placed.new(token, style, x, y, w, h, style.owner, char_offset) char_offset += token.length x += w line_height = [line_height, h].max end end flush_line.call @width = max_x @height = y end |