whirled_peas 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,35 @@
1
+ require_relative 'stroke'
2
+
3
+ module WhirledPeas
4
+ module UI
5
+ class Canvas
6
+ attr_reader :left, :top, :width, :height
7
+
8
+ def initialize(left, top, width, height)
9
+ @left = left
10
+ @top = top
11
+ @width = width
12
+ @height = height
13
+ end
14
+
15
+ def stroke(left, top, chars)
16
+ if left >= self.left + self.width || left + chars.length <= self.left
17
+ Stroke::EMPTY
18
+ elsif top < self.top || top >= self.top + self.height
19
+ Stroke::EMPTY
20
+ else
21
+ if left < self.left
22
+ chars = chars[self.left - left..-1]
23
+ left = self.left
24
+ end
25
+ num_chars = [self.left + self.width, left + chars.length].min - left
26
+ Stroke.new(left, top, Ansi.first(chars, num_chars))
27
+ end
28
+ end
29
+
30
+ def inspect
31
+ "Canvas(left=#{left}, top=#{top}, width=#{width}, height=#{height})"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,199 @@
1
+ require_relative 'settings'
2
+
3
+ module WhirledPeas
4
+ module UI
5
+ class Element
6
+ attr_accessor :preferred_width, :preferred_height
7
+ attr_reader :settings
8
+
9
+ def initialize(settings)
10
+ @settings = settings
11
+ end
12
+ end
13
+ private_constant :Element
14
+
15
+ class TextElement < Element
16
+ attr_reader :value
17
+
18
+ def initialize(settings)
19
+ super(TextSettings.merge(settings))
20
+ end
21
+
22
+ def value=(val)
23
+ @value = val
24
+ @preferred_width = settings.width || value.length
25
+ @preferred_height = 1
26
+ end
27
+
28
+ def inspect(indent='')
29
+ dims = unless preferred_width.nil?
30
+ "#{indent + ' '}Dimensions: #{preferred_width}x#{preferred_height}"
31
+ end
32
+ [
33
+ "#{indent}#{self.class.name}",
34
+ dims,
35
+ "#{indent + ' '}Settings",
36
+ settings.inspect(indent + ' ')
37
+ ].compact.join("\n")
38
+ end
39
+ end
40
+
41
+ class ComposableElement < Element
42
+ def initialize(settings)
43
+ super
44
+ end
45
+
46
+ def children
47
+ @children ||= []
48
+ end
49
+
50
+ def add_text(&block)
51
+ element = TextElement.new(settings)
52
+ element.value = yield nil, element.settings
53
+ children << element
54
+ end
55
+
56
+ def add_box(&block)
57
+ element = BoxElement.new(settings)
58
+ value = yield element, element.settings
59
+ children << element
60
+ if element.children.empty? && value.is_a?(String)
61
+ element.add_text { value }
62
+ end
63
+ end
64
+
65
+ def add_grid(&block)
66
+ element = GridElement.new(settings)
67
+ values = yield element, element.settings
68
+ children << element
69
+ if element.children.empty? && values.is_a?(Array)
70
+ values.each { |v| element.add_text { v } }
71
+ end
72
+ end
73
+
74
+ def inspect(indent='')
75
+ kids = children.map { |c| c.inspect(indent + ' ') }.join("\n")
76
+ dims = unless preferred_width.nil?
77
+ "#{indent + ' '}Dimensions: #{preferred_width}x#{preferred_height}"
78
+ end
79
+ [
80
+ "#{indent}#{self.class.name}",
81
+ dims,
82
+ "#{indent + ' '}Settings",
83
+ settings.inspect(indent + ' '),
84
+ "#{indent + ' '}Children",
85
+ kids
86
+ ].compact.join("\n")
87
+ end
88
+ end
89
+
90
+ class Template < ComposableElement
91
+ def initialize(settings=TemplateSettings.new)
92
+ super(settings)
93
+ end
94
+ end
95
+
96
+ class BoxElement < ComposableElement
97
+ attr_writer :content_width, :content_height
98
+
99
+ def initialize(settings)
100
+ super(BoxSettings.merge(settings))
101
+ end
102
+
103
+ def self.from_template(template, width, height)
104
+ box = new(template.settings)
105
+ template.children.each { |c| box.children << c }
106
+ box.content_width = box.preferred_width = width
107
+ box.content_height = box.preferred_height = height
108
+ box
109
+ end
110
+
111
+ def content_width
112
+ @content_width ||= begin
113
+ child_widths = children.map(&:preferred_width)
114
+ width = settings.horizontal_flow? ? child_widths.sum : (child_widths.max || 0)
115
+ [width, *settings.width].max
116
+ end
117
+ end
118
+
119
+ def preferred_width
120
+ @preferred_width ||= settings.margin.left +
121
+ (settings.border.left? ? 1 : 0) +
122
+ settings.padding.left +
123
+ content_width +
124
+ settings.padding.right +
125
+ (settings.border.right? ? 1 : 0) +
126
+ settings.margin.right
127
+ end
128
+
129
+ def content_height
130
+ @content_height ||= begin
131
+ child_heights = children.map(&:preferred_height)
132
+ settings.vertical_flow? ? child_heights.sum : (child_heights.max || 0)
133
+ end
134
+ end
135
+
136
+ def preferred_height
137
+ @preferred_height ||= settings.margin.top +
138
+ (settings.border.top? ? 1 : 0) +
139
+ settings.padding.top +
140
+ content_height +
141
+ settings.padding.bottom +
142
+ (settings.border.bottom? ? 1 : 0) +
143
+ settings.margin.bottom
144
+ end
145
+ end
146
+
147
+ class GridElement < ComposableElement
148
+ def initialize(settings)
149
+ super(GridSettings.merge(settings))
150
+ end
151
+
152
+ def col_width
153
+ return @col_width if @col_width
154
+ @col_width = 0
155
+ children.each do |child|
156
+ @col_width = child.preferred_width if child.preferred_width > @col_width
157
+ end
158
+ @col_width
159
+ end
160
+
161
+ def row_height
162
+ return @row_height if @row_height
163
+ @row_height = 0
164
+ children.each do |child|
165
+ @row_height = child.preferred_height if child.preferred_height > @row_height
166
+ end
167
+ @row_height
168
+ end
169
+
170
+
171
+ def preferred_width
172
+ settings.margin.left +
173
+ (settings.border.left? ? 1 : 0) +
174
+ settings.num_cols * (
175
+ settings.padding.left +
176
+ col_width +
177
+ settings.padding.right
178
+ ) +
179
+ (settings.num_cols - 1) * (settings.border.inner_vert? ? 1 : 0) +
180
+ (settings.border.right? ? 1 : 0) +
181
+ settings.margin.right
182
+ end
183
+
184
+ def preferred_height
185
+ num_rows = (children.length / settings.num_cols).ceil
186
+ settings.margin.top +
187
+ (settings.border.top? ? 1 : 0) +
188
+ num_rows * (
189
+ settings.padding.top +
190
+ row_height +
191
+ settings.padding.bottom
192
+ ) +
193
+ (num_rows - 1) * (settings.border.inner_horiz? ? 1 : 0) +
194
+ (settings.border.bottom? ? 1 : 0) +
195
+ settings.margin.bottom
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,283 @@
1
+ require_relative 'ansi'
2
+ require_relative 'canvas'
3
+ require_relative 'settings'
4
+
5
+ module WhirledPeas
6
+ module UI
7
+ DEBUG_SPACING = ARGV.include?('--debug-spacing')
8
+
9
+ class TextPainter
10
+ JUSTIFICATION = DEBUG_SPACING ? 'j' : ' '
11
+
12
+ def initialize(text, canvas)
13
+ @text = text
14
+ @canvas = canvas
15
+ end
16
+
17
+ def paint(&block)
18
+ yield canvas.stroke(canvas.left, canvas.top, justified)
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :text, :canvas
24
+
25
+ def visible
26
+ if text.value.length <= text.preferred_width
27
+ text.value
28
+ elsif text.settings.align == TextAlign::LEFT
29
+ text.value[0..text.preferred_width - 1]
30
+ elsif text.settings.align == TextAlign::CENTER
31
+ left_chop = (text.value.length - text.preferred_width) / 2
32
+ right_chop = text.value.length - text.preferred_width - left_chop
33
+ text.value[left_chop..-right_chop - 1]
34
+ else
35
+ text.value[-text.preferred_width..-1]
36
+ end
37
+ end
38
+
39
+ def justified
40
+ format_settings = [*text.settings.color, *text.settings.bg_color]
41
+ format_settings << TextFormat::BOLD if text.settings.bold?
42
+ format_settings << TextFormat::UNDERLINE if text.settings.underline?
43
+
44
+ ljust = case text.settings.align
45
+ when TextAlign::LEFT
46
+ 0
47
+ when TextAlign::CENTER
48
+ [0, (text.preferred_width - text.value.length) / 2].max
49
+ when TextAlign::RIGHT
50
+ [0, text.preferred_width - text.value.length].max
51
+ end
52
+ rjust = [0, text.preferred_width - text.value.length - ljust].max
53
+ Ansi.format(JUSTIFICATION * ljust, [*text.settings.bg_color]) +
54
+ Ansi.format(visible, format_settings) +
55
+ Ansi.format(JUSTIFICATION * rjust, [*text.settings.bg_color])
56
+ end
57
+ end
58
+ private_constant :TextPainter
59
+
60
+ class ContainerPainter
61
+ PADDING = DEBUG_SPACING ? 'p' : ' '
62
+
63
+ def initialize(container, canvas)
64
+ @container = container
65
+ @settings = container.settings
66
+ @canvas = canvas
67
+ end
68
+
69
+ def paint(&block)
70
+ return if container.num_rows == 0 || container.num_cols == 0
71
+ top = canvas.top + settings.margin.top
72
+ if settings.auto_margin?
73
+ left = canvas.left + (canvas.width - container.preferred_width) / 2
74
+ else
75
+ left = canvas.left + settings.margin.left
76
+ end
77
+ if settings.border.top?
78
+ yield canvas.stroke(left, top, top_border)
79
+ top += 1
80
+ end
81
+ container.num_rows.times do |row_num|
82
+ if row_num > 0 && settings.border.inner_horiz?
83
+ yield canvas.stroke(left, top, middle_border)
84
+ top += 1
85
+ end
86
+ (settings.padding.top + container.row_height + settings.padding.bottom).times do
87
+ yield canvas.stroke(left, top, content_line)
88
+ top += 1
89
+ end
90
+ end
91
+ if settings.border.bottom?
92
+ yield canvas.stroke(left, top, bottom_border)
93
+ top += 1
94
+ end
95
+ end
96
+
97
+ private
98
+
99
+ attr_reader :container, :settings, :canvas
100
+
101
+ def line_stroke(left_border, horiz_border, junc_border, right_border)
102
+ stroke = ''
103
+ stroke += left_border if settings.border.left?
104
+ container.num_cols.times do |col_num|
105
+ stroke += junc_border if col_num > 0 && settings.border.inner_horiz?
106
+ stroke += horiz_border * (container.col_width + settings.padding.left + settings.padding.right)
107
+ end
108
+ stroke += right_border if settings.border.right?
109
+ Ansi.format(stroke, [*settings.border.color, *settings.bg_color])
110
+ end
111
+
112
+ def top_border
113
+ line_stroke(
114
+ settings.border.style.top_left,
115
+ settings.border.style.top_horiz,
116
+ settings.border.style.top_junc,
117
+ settings.border.style.top_right
118
+ )
119
+ end
120
+
121
+ def content_line
122
+ line_stroke(
123
+ settings.border.style.left_vert,
124
+ PADDING,
125
+ settings.border.style.middle_vert,
126
+ settings.border.style.right_vert
127
+ )
128
+ end
129
+
130
+ def middle_border
131
+ line_stroke(
132
+ settings.border.style.left_junc,
133
+ settings.border.style.middle_horiz,
134
+ settings.border.style.cross_junc,
135
+ settings.border.style.right_junc
136
+ )
137
+ end
138
+
139
+ def bottom_border
140
+ line_stroke(
141
+ settings.border.style.bottom_left,
142
+ settings.border.style.bottom_horiz,
143
+ settings.border.style.bottom_junc,
144
+ settings.border.style.bottom_right
145
+ )
146
+ end
147
+ end
148
+
149
+ class BoxContainer
150
+ attr_reader :settings, :num_cols, :num_rows, :col_width, :row_height, :preferred_width
151
+
152
+ def initialize(box)
153
+ @settings = ContainerSettings.merge(box.settings)
154
+ @num_cols = 1
155
+ @num_rows = 1
156
+ @col_width = box.content_width
157
+ @row_height = box.content_height
158
+ @preferred_width = box.preferred_width
159
+ end
160
+ end
161
+
162
+ class BoxPainter
163
+ def initialize(box, canvas)
164
+ @box = box
165
+ @canvas = canvas
166
+ end
167
+
168
+ def paint(&block)
169
+ container = BoxContainer.new(box)
170
+ ContainerPainter.new(container, canvas).paint(&block)
171
+ top = canvas.top + box.settings.margin.top + (box.settings.border.top? ? 1 : 0) + box.settings.padding.top
172
+ if box.settings.auto_margin?
173
+ margin = (canvas.width - box.preferred_width) / 2
174
+ else
175
+ margin = box.settings.margin.left
176
+ end
177
+ left = canvas.left + margin + (box.settings.border.left? ? 1 : 0) + box.settings.padding.left
178
+ greedy_width = box.settings.vertical_flow? || box.children.length == 1
179
+ children = box.children
180
+ children = children.reverse if box.settings.reverse_flow?
181
+ children.each do |child|
182
+ if greedy_width
183
+ width = box.content_width
184
+ height = child.preferred_height
185
+ else
186
+ width = child.preferred_width
187
+ height = box.content_height
188
+ end
189
+ child_canvas = Canvas.new(left, top, width, height)
190
+ Painter.paint(child, child_canvas, &block)
191
+ if box.settings.horizontal_flow?
192
+ left += child.preferred_width
193
+ else
194
+ top += child.preferred_height
195
+ end
196
+ end
197
+ end
198
+
199
+ private
200
+
201
+ attr_reader :box, :canvas
202
+ end
203
+
204
+ class GridContainer
205
+ attr_reader :settings, :num_cols, :num_rows, :col_width, :row_height, :preferred_width
206
+
207
+ def initialize(grid, num_cols, num_rows)
208
+ @settings = ContainerSettings.merge(grid.settings)
209
+ @num_cols = num_cols
210
+ @num_rows = num_rows
211
+ @col_width = grid.col_width
212
+ @row_height = grid.row_height
213
+ @preferred_width = grid.preferred_width
214
+ end
215
+ end
216
+
217
+ class GridPainter
218
+ def initialize(grid, canvas)
219
+ @grid = grid
220
+ @canvas = canvas
221
+ available_width = grid.preferred_width - (grid.settings.margin.left || 0) - (grid.settings.margin.right || 0)
222
+ @num_cols = grid.settings.num_cols || (available_width - (grid.settings.border.left? ? 1 : 0) - (grid.settings.border.right? ? 1 : 0) + (grid.border.inner_vert ? 1 : 0)) / (col_width + grid.settings.padding.left + grid.settings.right + (grid.border.inner_vert ? 1 : 0))
223
+ end
224
+
225
+ def paint(&block)
226
+ return if grid.children.empty?
227
+
228
+ container = GridContainer.new(grid, num_cols, (grid.children.length.to_f / num_cols).ceil)
229
+ ContainerPainter.new(container, canvas).paint(&block)
230
+
231
+ children = if grid.settings.transpose?
232
+ grid.children.length.times.map do |i|
233
+ grid.children[(i * num_cols) % grid.children.length + i / (grid.children.length / num_cols)]
234
+ end.compact
235
+ else
236
+ grid.children
237
+ end
238
+
239
+ top = canvas.top + grid.settings.margin.top + (grid.settings.border.top? ? 1 : 0) + grid.settings.padding.top
240
+ if grid.settings.auto_margin?
241
+ margin = (canvas.width - grid.preferred_width) / 2
242
+ else
243
+ margin = grid.settings.margin.left
244
+ end
245
+ left = canvas.left + margin + (grid.settings.border.left? ? 1 : 0) + grid.settings.padding.left
246
+ grid_height = grid.settings.padding.top + grid.row_height + grid.settings.padding.bottom + (grid.settings.border.inner_horiz? ? 1 : 0)
247
+ grid_width = grid.settings.padding.left + grid.col_width + grid.settings.padding.right + (grid.settings.border.inner_vert? ? 1 : 0)
248
+ children.each_slice(num_cols).each.with_index do |row, row_num|
249
+ row_top = top + row_num * grid_height
250
+ row.each.with_index do |element, col_num|
251
+ col_left = left + col_num * grid_width
252
+ child_canvas = Canvas.new(
253
+ col_left,
254
+ row_top,
255
+ element.preferred_width,
256
+ element.preferred_height
257
+ )
258
+ Painter.paint(element, child_canvas, &block)
259
+ end
260
+ end
261
+ end
262
+
263
+ private
264
+
265
+ attr_reader :grid, :canvas, :num_cols
266
+ end
267
+
268
+ module Painter
269
+ PAINTERS = {
270
+ TextElement => TextPainter,
271
+ BoxElement => BoxPainter,
272
+ GridElement => GridPainter,
273
+ }
274
+
275
+ def self.paint(element, canvas, &block)
276
+ if element.is_a?(Template)
277
+ element = BoxElement.from_template(element, canvas.width, canvas.height)
278
+ end
279
+ PAINTERS[element.class].new(element, canvas).paint(&block)
280
+ end
281
+ end
282
+ end
283
+ end