/lib/prawn/table/cell.rb
Ruby | 395 lines | 202 code | 57 blank | 136 comment | 14 complexity | 9df7c84294fef9ee82a50ecdaa72b65e MD5 | raw file
1# encoding: utf-8
2
3# cell.rb: Table cell drawing.
4#
5# Copyright December 2009, Gregory Brown and Brad Ediger. All Rights Reserved.
6#
7# This is free software. Please see the LICENSE and COPYING files for details.
8
9module Prawn
10 class Document
11
12 # Instantiates and draws a cell on the document.
13 #
14 # cell(:content => "Hello world!", :at => [12, 34])
15 #
16 # See Prawn::Table::Cell.make for full options.
17 #
18 def cell(options={})
19 cell = Table::Cell.make(self, options.delete(:content), options)
20 cell.draw
21 cell
22 end
23
24 # Set up, but do not draw, a cell. Useful for creating cells with
25 # formatting options to be inserted into a Table. Call +draw+ on the
26 # resulting Cell to ink it.
27 #
28 # See the documentation on Prawn::Cell for details on the arguments.
29 #
30 def make_cell(content, options={})
31 Prawn::Table::Cell.make(self, content, options)
32 end
33
34 end
35
36 class Table
37
38 # A Cell is a rectangular area of the page into which content is drawn. It
39 # has a framework for sizing itself and adding padding and simple styling.
40 # There are several standard Cell subclasses that handle things like text,
41 # Tables, and (in the future) stamps, images, and arbitrary content.
42 #
43 # Cells are a basic building block for table support (see Prawn::Table).
44 #
45 # Please subclass me if you want new content types! I'm designed to be very
46 # extensible. See the different standard Cell subclasses in
47 # lib/prawn/table/cell/*.rb for a template.
48 #
49 class Cell
50
51 # Amount of dead space (in PDF points) inside the borders but outside the
52 # content. Padding defaults to 5pt.
53 #
54 attr_reader :padding
55
56 # If provided, the minimum width that this cell will permit.
57 #
58 def min_width
59 set_width_constraints
60 @min_width
61 end
62
63 # If provided, the maximum width that this cell can be drawn in.
64 #
65 def max_width
66 set_width_constraints
67 @max_width
68 end
69
70 # Manually specify the cell's height.
71 #
72 attr_writer :height
73
74 # Specifies which borders to enable. Must be an array of zero or more of:
75 # <tt>[:left, :right, :top, :bottom]</tt>.
76 #
77 attr_accessor :borders
78
79 # Specifies the width, in PDF points, of the cell's borders.
80 #
81 attr_accessor :border_width
82
83 # Specifies the color of the cell borders. Given in HTML RGB format, e.g.,
84 # "ccffff".
85 #
86 attr_accessor :border_color
87
88 # Specifies the content for the cell. Must be a "cellable" object. See the
89 # "Data" section of the Prawn::Table documentation for details on cellable
90 # objects.
91 #
92 attr_accessor :content
93
94 # The background color, if any, for this cell. Specified in HTML RGB
95 # format, e.g., "ccffff". The background is drawn under the whole cell,
96 # including any padding.
97 #
98 attr_accessor :background_color
99
100 # Instantiates a Cell based on the given options. The particular class of
101 # cell returned depends on the :content argument. See the Prawn::Table
102 # documentation under "Data" for allowable content types.
103 #
104 def self.make(pdf, content, options={})
105 at = options.delete(:at) || [0, pdf.cursor]
106 content = "" if content.nil?
107
108 if content.is_a?(Hash)
109 options.update(content)
110 content = options[:content]
111 else
112 options[:content] = content
113 end
114
115 case content
116 when Prawn::Table::Cell
117 content
118 when String
119 Cell::Text.new(pdf, at, options)
120 when Prawn::Table
121 Cell::Subtable.new(pdf, at, options)
122 when Array
123 subtable = Prawn::Table.new(options[:content], pdf, {})
124 Cell::Subtable.new(pdf, at, options.merge(:content => subtable))
125 else
126 # TODO: other types of content
127 raise ArgumentError, "Content type not recognized: #{content.inspect}"
128 end
129 end
130
131 # A small amount added to the bounding box width to cover over floating-
132 # point errors when round-tripping from content_width to width and back.
133 # This does not change cell positioning; it only slightly expands each
134 # cell's bounding box width so that rounding error does not prevent a cell
135 # from rendering.
136 #
137 FPTolerance = 1
138
139 # Sets up a cell on the document +pdf+, at the given x/y location +point+,
140 # with the given +options+. Cell, like Table, follows the "options set
141 # accessors" paradigm (see "Options" under the Table documentation), so
142 # any cell accessor <tt>cell.foo = :bar</tt> can be set by providing the
143 # option <tt>:foo => :bar</tt> here.
144 #
145 def initialize(pdf, point, options={})
146 @pdf = pdf
147 @point = point
148
149 # Set defaults; these can be changed by options
150 @padding = [5, 5, 5, 5]
151 @borders = [:top, :bottom, :left, :right]
152 @border_width = 1
153 @border_color = '000000'
154
155 options.each { |k, v| send("#{k}=", v) }
156 end
157
158 # Supports setting multiple properties at once.
159 #
160 # cell.style(:padding => 0, :border_width => 2)
161 #
162 # is the same as:
163 #
164 # cell.padding = 0
165 # cell.border_width = 2
166 #
167 def style(options={}, &block)
168 options.each { |k, v| send("#{k}=", v) }
169
170 # The block form supports running a single block for multiple cells, as
171 # in Cells#style.
172 block.call(self) if block
173 end
174
175 # Returns the cell's width in points, inclusive of padding.
176 #
177 def width
178 # We can't ||= here because the FP error accumulates on the round-trip
179 # from #content_width.
180 @width || (content_width + padding_left + padding_right)
181 end
182
183 # Manually sets the cell's width, inclusive of padding.
184 #
185 def width=(w)
186 @width = @min_width = @max_width = w
187 end
188
189 # Returns the width of the bare content in the cell, excluding padding.
190 #
191 def content_width
192 if @width # manually set
193 return @width - padding_left - padding_right
194 end
195
196 natural_content_width
197 end
198
199 # Returns the width this cell would naturally take on, absent other
200 # constraints. Must be implemented in subclasses.
201 #
202 def natural_content_width
203 raise NotImplementedError,
204 "subclasses must implement natural_content_width"
205 end
206
207 # Returns the cell's height in points, inclusive of padding.
208 #
209 def height
210 # We can't ||= here because the FP error accumulates on the round-trip
211 # from #content_height.
212 @height || (content_height + padding_top + padding_bottom)
213 end
214
215 # Returns the height of the bare content in the cell, excluding padding.
216 #
217 def content_height
218 if @height # manually set
219 return @height - padding_top - padding_bottom
220 end
221
222 natural_content_height
223 end
224
225 # Returns the height this cell would naturally take on, absent
226 # constraints. Must be implemented in subclasses.
227 #
228 def natural_content_height
229 raise NotImplementedError,
230 "subclasses must implement natural_content_height"
231 end
232
233 # Draws the cell onto the document. Pass in a point [x,y] to override the
234 # location at which the cell is drawn.
235 #
236 def draw(pt=[x, y])
237 set_width_constraints
238
239 draw_background(pt)
240 draw_borders(pt)
241 @pdf.bounding_box([pt[0] + padding_left, pt[1] - padding_top],
242 :width => content_width + FPTolerance,
243 :height => content_height + FPTolerance) do
244 draw_content
245 end
246 end
247
248 # x-position of the cell within the parent bounds.
249 #
250 def x
251 @point[0]
252 end
253
254 # Set the x-position of the cell within the parent bounds.
255 #
256 def x=(val)
257 @point[0] = val
258 end
259
260 # y-position of the cell within the parent bounds.
261 #
262 def y
263 @point[1]
264 end
265
266 # Set the y-position of the cell within the parent bounds.
267 #
268 def y=(val)
269 @point[1] = val
270 end
271
272 # Sets padding on this cell. The argument can be one of:
273 #
274 # * an integer (sets all padding)
275 # * a two-element array [vertical, horizontal]
276 # * a three-element array [top, horizontal, bottom]
277 # * a four-element array [top, right, bottom, left]
278 #
279 def padding=(pad)
280 @padding = case
281 when pad.nil?
282 [0, 0, 0, 0]
283 when Numeric === pad # all padding
284 [pad, pad, pad, pad]
285 when pad.length == 2 # vert, horiz
286 [pad[0], pad[1], pad[0], pad[1]]
287 when pad.length == 3 # top, horiz, bottom
288 [pad[0], pad[1], pad[2], pad[1]]
289 when pad.length == 4 # top, right, bottom, left
290 [pad[0], pad[1], pad[2], pad[3]]
291 else
292 raise ArgumentError, ":padding must be a number or an array [v,h] " +
293 "or [t,r,b,l]"
294 end
295 end
296
297 protected
298
299 # Sets the cell's minimum and maximum width. Deferred until requested
300 # because padding and size can change.
301 #
302 def set_width_constraints
303 @min_width ||= padding_left + padding_right
304 @max_width ||= @pdf.bounds.width
305 end
306
307 # Draws the cell's background color.
308 #
309 def draw_background(pt)
310 x, y = pt
311 margin = @border_width / 2
312 if @background_color
313 @pdf.mask(:fill_color) do
314 @pdf.fill_color @background_color
315 h = @borders.include?(:bottom) ? height - (2*margin) :
316 height + margin
317 @pdf.fill_rectangle [x, y], width, h
318 end
319 end
320 end
321
322 # Draws borders around the cell. Borders are centered on the bounds of
323 # the cell outside of any padding, so the caller is responsible for
324 # setting appropriate padding to ensure the border does not overlap with
325 # cell content.
326 #
327 def draw_borders(pt)
328 x, y = pt
329 return if @border_width <= 0
330 # Draw left / right borders one-half border width beyond the center of
331 # the corner, so that the corners end up square.
332 margin = @border_width / 2.0
333
334 @pdf.mask(:line_width, :stroke_color) do
335 @pdf.line_width = @border_width
336 @pdf.stroke_color = @border_color if @border_color
337
338 @borders.each do |border|
339 from, to = case border
340 when :top
341 [[x, y], [x+width, y]]
342 when :bottom
343 [[x, y-height], [x+width, y-height]]
344 when :left
345 [[x, y+margin], [x, y-height-margin]]
346 when :right
347 [[x+width, y+margin], [x+width, y-height-margin]]
348 end
349 @pdf.stroke_line(from, to)
350 end
351 end
352 end
353
354 # Draws cell content within the cell's bounding box. Must be implemented
355 # in subclasses.
356 #
357 def draw_content
358 raise NotImplementedError, "subclasses must implement draw_content"
359 end
360
361 def padding_top
362 @padding[0]
363 end
364
365 def padding_top=(val)
366 @padding[0] = val
367 end
368
369 def padding_right
370 @padding[1]
371 end
372
373 def padding_right=(val)
374 @padding[1] = val
375 end
376
377 def padding_bottom
378 @padding[2]
379 end
380
381 def padding_bottom=(val)
382 @padding[2] = val
383 end
384
385 def padding_left
386 @padding[3]
387 end
388
389 def padding_left=(val)
390 @padding[3] = val
391 end
392
393 end
394 end
395end