/lib/prawn/table/cell.rb
Ruby | 395 lines | 202 code | 57 blank | 136 comment | 14 complexity | 9df7c84294fef9ee82a50ecdaa72b65e MD5 | raw file
- # encoding: utf-8
- # cell.rb: Table cell drawing.
- #
- # Copyright December 2009, Gregory Brown and Brad Ediger. All Rights Reserved.
- #
- # This is free software. Please see the LICENSE and COPYING files for details.
- module Prawn
- class Document
- # Instantiates and draws a cell on the document.
- #
- # cell(:content => "Hello world!", :at => [12, 34])
- #
- # See Prawn::Table::Cell.make for full options.
- #
- def cell(options={})
- cell = Table::Cell.make(self, options.delete(:content), options)
- cell.draw
- cell
- end
-
- # Set up, but do not draw, a cell. Useful for creating cells with
- # formatting options to be inserted into a Table. Call +draw+ on the
- # resulting Cell to ink it.
- #
- # See the documentation on Prawn::Cell for details on the arguments.
- #
- def make_cell(content, options={})
- Prawn::Table::Cell.make(self, content, options)
- end
- end
- class Table
-
- # A Cell is a rectangular area of the page into which content is drawn. It
- # has a framework for sizing itself and adding padding and simple styling.
- # There are several standard Cell subclasses that handle things like text,
- # Tables, and (in the future) stamps, images, and arbitrary content.
- #
- # Cells are a basic building block for table support (see Prawn::Table).
- #
- # Please subclass me if you want new content types! I'm designed to be very
- # extensible. See the different standard Cell subclasses in
- # lib/prawn/table/cell/*.rb for a template.
- #
- class Cell
- # Amount of dead space (in PDF points) inside the borders but outside the
- # content. Padding defaults to 5pt.
- #
- attr_reader :padding
- # If provided, the minimum width that this cell will permit.
- #
- def min_width
- set_width_constraints
- @min_width
- end
-
- # If provided, the maximum width that this cell can be drawn in.
- #
- def max_width
- set_width_constraints
- @max_width
- end
- # Manually specify the cell's height.
- #
- attr_writer :height
- # Specifies which borders to enable. Must be an array of zero or more of:
- # <tt>[:left, :right, :top, :bottom]</tt>.
- #
- attr_accessor :borders
- # Specifies the width, in PDF points, of the cell's borders.
- #
- attr_accessor :border_width
- # Specifies the color of the cell borders. Given in HTML RGB format, e.g.,
- # "ccffff".
- #
- attr_accessor :border_color
- # Specifies the content for the cell. Must be a "cellable" object. See the
- # "Data" section of the Prawn::Table documentation for details on cellable
- # objects.
- #
- attr_accessor :content
- # The background color, if any, for this cell. Specified in HTML RGB
- # format, e.g., "ccffff". The background is drawn under the whole cell,
- # including any padding.
- #
- attr_accessor :background_color
- # Instantiates a Cell based on the given options. The particular class of
- # cell returned depends on the :content argument. See the Prawn::Table
- # documentation under "Data" for allowable content types.
- #
- def self.make(pdf, content, options={})
- at = options.delete(:at) || [0, pdf.cursor]
- content = "" if content.nil?
- if content.is_a?(Hash)
- options.update(content)
- content = options[:content]
- else
- options[:content] = content
- end
- case content
- when Prawn::Table::Cell
- content
- when String
- Cell::Text.new(pdf, at, options)
- when Prawn::Table
- Cell::Subtable.new(pdf, at, options)
- when Array
- subtable = Prawn::Table.new(options[:content], pdf, {})
- Cell::Subtable.new(pdf, at, options.merge(:content => subtable))
- else
- # TODO: other types of content
- raise ArgumentError, "Content type not recognized: #{content.inspect}"
- end
- end
- # A small amount added to the bounding box width to cover over floating-
- # point errors when round-tripping from content_width to width and back.
- # This does not change cell positioning; it only slightly expands each
- # cell's bounding box width so that rounding error does not prevent a cell
- # from rendering.
- #
- FPTolerance = 1
- # Sets up a cell on the document +pdf+, at the given x/y location +point+,
- # with the given +options+. Cell, like Table, follows the "options set
- # accessors" paradigm (see "Options" under the Table documentation), so
- # any cell accessor <tt>cell.foo = :bar</tt> can be set by providing the
- # option <tt>:foo => :bar</tt> here.
- #
- def initialize(pdf, point, options={})
- @pdf = pdf
- @point = point
- # Set defaults; these can be changed by options
- @padding = [5, 5, 5, 5]
- @borders = [:top, :bottom, :left, :right]
- @border_width = 1
- @border_color = '000000'
- options.each { |k, v| send("#{k}=", v) }
- end
- # Supports setting multiple properties at once.
- #
- # cell.style(:padding => 0, :border_width => 2)
- #
- # is the same as:
- #
- # cell.padding = 0
- # cell.border_width = 2
- #
- def style(options={}, &block)
- options.each { |k, v| send("#{k}=", v) }
- # The block form supports running a single block for multiple cells, as
- # in Cells#style.
- block.call(self) if block
- end
- # Returns the cell's width in points, inclusive of padding.
- #
- def width
- # We can't ||= here because the FP error accumulates on the round-trip
- # from #content_width.
- @width || (content_width + padding_left + padding_right)
- end
- # Manually sets the cell's width, inclusive of padding.
- #
- def width=(w)
- @width = @min_width = @max_width = w
- end
- # Returns the width of the bare content in the cell, excluding padding.
- #
- def content_width
- if @width # manually set
- return @width - padding_left - padding_right
- end
- natural_content_width
- end
- # Returns the width this cell would naturally take on, absent other
- # constraints. Must be implemented in subclasses.
- #
- def natural_content_width
- raise NotImplementedError,
- "subclasses must implement natural_content_width"
- end
- # Returns the cell's height in points, inclusive of padding.
- #
- def height
- # We can't ||= here because the FP error accumulates on the round-trip
- # from #content_height.
- @height || (content_height + padding_top + padding_bottom)
- end
- # Returns the height of the bare content in the cell, excluding padding.
- #
- def content_height
- if @height # manually set
- return @height - padding_top - padding_bottom
- end
-
- natural_content_height
- end
- # Returns the height this cell would naturally take on, absent
- # constraints. Must be implemented in subclasses.
- #
- def natural_content_height
- raise NotImplementedError,
- "subclasses must implement natural_content_height"
- end
- # Draws the cell onto the document. Pass in a point [x,y] to override the
- # location at which the cell is drawn.
- #
- def draw(pt=[x, y])
- set_width_constraints
- draw_background(pt)
- draw_borders(pt)
- @pdf.bounding_box([pt[0] + padding_left, pt[1] - padding_top],
- :width => content_width + FPTolerance,
- :height => content_height + FPTolerance) do
- draw_content
- end
- end
- # x-position of the cell within the parent bounds.
- #
- def x
- @point[0]
- end
- # Set the x-position of the cell within the parent bounds.
- #
- def x=(val)
- @point[0] = val
- end
- # y-position of the cell within the parent bounds.
- #
- def y
- @point[1]
- end
- # Set the y-position of the cell within the parent bounds.
- #
- def y=(val)
- @point[1] = val
- end
- # Sets padding on this cell. The argument can be one of:
- #
- # * an integer (sets all padding)
- # * a two-element array [vertical, horizontal]
- # * a three-element array [top, horizontal, bottom]
- # * a four-element array [top, right, bottom, left]
- #
- def padding=(pad)
- @padding = case
- when pad.nil?
- [0, 0, 0, 0]
- when Numeric === pad # all padding
- [pad, pad, pad, pad]
- when pad.length == 2 # vert, horiz
- [pad[0], pad[1], pad[0], pad[1]]
- when pad.length == 3 # top, horiz, bottom
- [pad[0], pad[1], pad[2], pad[1]]
- when pad.length == 4 # top, right, bottom, left
- [pad[0], pad[1], pad[2], pad[3]]
- else
- raise ArgumentError, ":padding must be a number or an array [v,h] " +
- "or [t,r,b,l]"
- end
- end
- protected
- # Sets the cell's minimum and maximum width. Deferred until requested
- # because padding and size can change.
- #
- def set_width_constraints
- @min_width ||= padding_left + padding_right
- @max_width ||= @pdf.bounds.width
- end
- # Draws the cell's background color.
- #
- def draw_background(pt)
- x, y = pt
- margin = @border_width / 2
- if @background_color
- @pdf.mask(:fill_color) do
- @pdf.fill_color @background_color
- h = @borders.include?(:bottom) ? height - (2*margin) :
- height + margin
- @pdf.fill_rectangle [x, y], width, h
- end
- end
- end
- # Draws borders around the cell. Borders are centered on the bounds of
- # the cell outside of any padding, so the caller is responsible for
- # setting appropriate padding to ensure the border does not overlap with
- # cell content.
- #
- def draw_borders(pt)
- x, y = pt
- return if @border_width <= 0
- # Draw left / right borders one-half border width beyond the center of
- # the corner, so that the corners end up square.
- margin = @border_width / 2.0
- @pdf.mask(:line_width, :stroke_color) do
- @pdf.line_width = @border_width
- @pdf.stroke_color = @border_color if @border_color
- @borders.each do |border|
- from, to = case border
- when :top
- [[x, y], [x+width, y]]
- when :bottom
- [[x, y-height], [x+width, y-height]]
- when :left
- [[x, y+margin], [x, y-height-margin]]
- when :right
- [[x+width, y+margin], [x+width, y-height-margin]]
- end
- @pdf.stroke_line(from, to)
- end
- end
- end
- # Draws cell content within the cell's bounding box. Must be implemented
- # in subclasses.
- #
- def draw_content
- raise NotImplementedError, "subclasses must implement draw_content"
- end
- def padding_top
- @padding[0]
- end
-
- def padding_top=(val)
- @padding[0] = val
- end
- def padding_right
- @padding[1]
- end
-
- def padding_right=(val)
- @padding[1] = val
- end
- def padding_bottom
- @padding[2]
- end
-
- def padding_bottom=(val)
- @padding[2] = val
- end
- def padding_left
- @padding[3]
- end
-
- def padding_left=(val)
- @padding[3] = val
- end
- end
- end
- end