lib/hexapdf/layout/box.rb in hexapdf-0.23.0 vs lib/hexapdf/layout/box.rb in hexapdf-0.24.0

- old
+ new

@@ -2,11 +2,11 @@ # #-- # This file is part of HexaPDF. # # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby -# Copyright (C) 2014-2021 Thomas Leitner +# Copyright (C) 2014-2022 Thomas Leitner # # HexaPDF is free software: you can redistribute it and/or modify it # under the terms of the GNU Affero General Public License version 3 as # published by the Free Software Foundation with the addition of the # following permission added to Section 15 as permitted in Section 7(a): @@ -38,25 +38,50 @@ module HexaPDF module Layout # The base class for all layout boxes. # + # == Box Model + # # HexaPDF uses the following box model: # # * Each box can specify a width and height. Padding and border are inside, the margin outside # of this rectangle. # # * The #content_width and #content_height accessors can be used to get the width and height of # the content box without padding and the border. # # * If width or height is set to zero, they are determined automatically during layouting. + # + # + # == Subclasses + # + # Each subclass should only take keyword arguments on initialization so that the boxes can be + # instantiated from the common convenience method HexaPDF::Document::Layout#box. To use this + # facility subclasses need to be registered with the configuration option 'layout.boxes.map'. + # + # The methods #fit, #split or #split_content, and #draw or #draw_content need to be customized + # according to the subclass's use case. + # + # #fit:: This method should return +true+ if fitting was successful. Additionally, the + # @fit_successful instance variable needs to be set to the fit result as it is used in + # #split. + # + # #split:: This method splits the content so that the available space is used as good as + # possible. The default implementation should be fine for most use-cases, so only + # #split_content needs to be implemented. The method #create_split_box should be used + # for getting a basic cloned box. + # + # #draw:: This method draws the content and the default implementation already handles things + # like drawing the border and background. Therefore it's best to implement #draw_content + # which should just draw the content. class Box # Creates a new Box object, using the provided block as drawing block (see ::new). # # If +content_box+ is +true+, the width and height are taken to mean the content width and - # height and the style's padding and border are removed from them appropriately. + # height and the style's padding and border are added to them appropriately. # # The +style+ argument defines the Style object (see Style::create for details) for the box. # Any additional keyword arguments have to be style properties and are applied to the style # object. def self.create(width: 0, height: 0, content_box: false, style: nil, **style_properties, &block) @@ -100,12 +125,24 @@ def initialize(width: 0, height: 0, style: nil, &block) @width = @initial_width = width @height = @initial_height = height @style = Style.create(style) @draw_block = block + @fit_successful = false + @split_box = false end + # Returns +true+ if this is a split box, i.e. the rest of another box after it was split. + def split_box? + @split_box + end + + # Returns +false+ since a basic box doesn't support the 'position' style property value :flow. + def supports_position_flow? + false + end + # The width of the content box, i.e. without padding and/or borders. def content_width width = @width - reserved_width width < 0 ? 0 : width end @@ -121,36 +158,45 @@ # The default implementation uses the whole available space for width and height if they were # initially set to 0. Otherwise the specified dimensions are used. def fit(available_width, available_height, _frame) @width = (@initial_width > 0 ? @initial_width : available_width) @height = (@initial_height > 0 ? @initial_height : available_height) - @width <= available_width && @height <= available_height + @fit_successful = (@width <= available_width && @height <= available_height) end # Tries to split the box into two, the first of which needs to fit into the available space, # and returns the parts as array. # - # In many cases the first box in the list will be this box, meaning that even when #fit fails, - # a part of the box may still fit. Note that #fit may not be called if the first box is this - # box since it is assumed that it is already fitted. If not even a part of this box fits into - # the available space, +nil+ should be returned as the first array element. + # If the first item in the result array is not +nil+, it needs to be this box and it means + # that even when #fit fails, a part of the box may still fit. Note that #fit should not be + # called before #draw on the first box since it is already fitted. If not even a part of this + # box fits into the available space, +nil+ should be returned as the first array element. # # Possible return values: # # [self]:: The box fully fits into the available space. # [nil, self]:: The box can't be split or no part of the box fits into the available space. # [self, new_box]:: A part of the box fits and a new box is returned for the rest. # - # This default implementation provides no splitting functionality. - def split(_available_width, _available_height, _frame) - [nil, self] + # This default implementation provides the basic functionality based on the #fit result that + # should be sufficient for most subclasses; only #split_content needs to be implemented if + # necessary. + def split(available_width, available_height, frame) + if @fit_successful + [self, nil] + elsif (style.position != :flow && (@width > available_width || @height > available_height)) || + content_height == 0 || content_width == 0 + [nil, self] + else + split_content(available_width, available_height, frame) + end end # Draws the content of the box onto the canvas at the position (x, y). # # The coordinate system is translated so that the origin is at the bottom left corner of the - # **content box** during the drawing operations. + # **content box** during the drawing operations when +@draw_block+ is used. # # The block specified when creating the box is invoked with the canvas and the box as # arguments. Subclasses can specify an on-demand drawing method by setting the +@draw_block+ # instance variable to +nil+ or a valid block. This is useful to avoid unnecessary set-up # operations when the block does nothing. @@ -163,15 +209,11 @@ end style.underlays.draw(canvas, x, y, self) if style.underlays? style.border.draw(canvas, x, y, width, height) if style.border? - cx = x - cy = y - (cx += style.padding.left; cy += style.padding.bottom) if style.padding? - (cx += style.border.width.left; cy += style.border.width.bottom) if style.border? - draw_content(canvas, cx, cy) + draw_content(canvas, x + reserved_width_left, y + reserved_height_bottom) style.overlays.draw(canvas, x, y, self) if style.overlays? end # Returns +true+ if no drawing operations are performed. @@ -185,29 +227,86 @@ private # Returns the width that is reserved by the padding and border style properties. def reserved_width - result = 0 - result += style.padding.left + style.padding.right if style.padding? - result += style.border.width.left + style.border.width.right if style.border? - result + reserved_width_left + reserved_width_right end # Returns the height that is reserved by the padding and border style properties. def reserved_height + reserved_height_top + reserved_height_bottom + end + + # Returns the width that is reserved by the padding and the border style properties on the + # left side of the box. + def reserved_width_left result = 0 - result += style.padding.top + style.padding.bottom if style.padding? - result += style.border.width.top + style.border.width.bottom if style.border? + result += style.padding.left if style.padding? + result += style.border.width.left if style.border? result end + # Returns the width that is reserved by the padding and the border style properties on the + # right side of the box. + def reserved_width_right + result = 0 + result += style.padding.right if style.padding? + result += style.border.width.right if style.border? + result + end + + # Returns the height that is reserved by the padding and the border style properties on the + # top side of the box. + def reserved_height_top + result = 0 + result += style.padding.top if style.padding? + result += style.border.width.top if style.border? + result + end + + # Returns the height that is reserved by the padding and the border style properties on the + # bottom side of the box. + def reserved_height_bottom + result = 0 + result += style.padding.bottom if style.padding? + result += style.border.width.bottom if style.border? + result + end + # Draws the content of the box at position [x, y] which is the bottom-left corner of the # content box. def draw_content(canvas, x, y) if @draw_block canvas.translate(x, y) { @draw_block.call(canvas, self) } end + end + + # Splits the content of the box. + # + # This is just a stub implementation, returning [nil, self] since we can't know how to split + # the content when it didn't fit. + # + # Subclasses that support splitting content need to provide an appropriate implementation and + # use #create_split_box to create a cloned box to supply as the second argument. + def split_content(_available_width, _available_height, _frame) + [nil, self] + end + + # Creates a new box based on this one and resets the data back to their original values. + # + # The variable +@split_box+ is set to +split_box_value+ (defaults to +true+) to make the new + # box aware that it is a split box. If needed, subclasses can set the variable to other truthy + # values to convey more meaning. + # + # This method should be used by subclasses to create their split box. + def create_split_box(split_box_value: true) + box = clone + box.instance_variable_set(:@width, @initial_width) + box.instance_variable_set(:@height, @initial_height) + box.instance_variable_set(:@fit_successful, nil) + box.instance_variable_set(:@split_box, split_box_value) + box end end end