PageRenderTime 72ms CodeModel.GetById 15ms app.highlight 47ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/prawn/table/cell.rb

https://github.com/seyfcom/prawn
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