whirled_peas 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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