-
-
Notifications
You must be signed in to change notification settings - Fork 70
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Layout::ContainerBox for laying out boxes together
- Loading branch information
Showing
5 changed files
with
249 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
# -*- encoding: utf-8; frozen_string_literal: true -*- | ||
# | ||
#-- | ||
# This file is part of HexaPDF. | ||
# | ||
# HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby | ||
# Copyright (C) 2014-2023 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): | ||
# FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY | ||
# THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON | ||
# INFRINGEMENT OF THIRD PARTY RIGHTS. | ||
# | ||
# HexaPDF is distributed in the hope that it will be useful, but WITHOUT | ||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | ||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public | ||
# License for more details. | ||
# | ||
# You should have received a copy of the GNU Affero General Public License | ||
# along with HexaPDF. If not, see <http://www.gnu.org/licenses/>. | ||
# | ||
# The interactive user interfaces in modified source and object code | ||
# versions of HexaPDF must display Appropriate Legal Notices, as required | ||
# under Section 5 of the GNU Affero General Public License version 3. | ||
# | ||
# In accordance with Section 7(b) of the GNU Affero General Public | ||
# License, a covered work must retain the producer line in every PDF that | ||
# is created or manipulated using HexaPDF. | ||
# | ||
# If the GNU Affero General Public License doesn't fit your need, | ||
# commercial licenses are available at <https://gettalong.at/hexapdf/>. | ||
#++ | ||
require 'hexapdf/layout/box' | ||
require 'hexapdf/layout/box_fitter' | ||
require 'hexapdf/layout/frame' | ||
|
||
module HexaPDF | ||
module Layout | ||
|
||
# This is a simple container box for laying out a number of boxes together. It is registered | ||
# under the :container name. | ||
# | ||
# The box does not support the value :flow for the style property position, so the child boxes | ||
# are laid out in the current region only. Since the boxes should be laid out together, if any | ||
# box doesn't fit, the whole container doesn't fit. Splitting the container is also not possible | ||
# for the same reason. | ||
# | ||
# By default the child boxes are laid out from top to bottom by default. By appropriately | ||
# setting the style properties 'mask_mode', 'align' and 'valign', it is possible to lay out the | ||
# children bottom to top, left to right, or right to left: | ||
# | ||
# * The standard top to bottom layout: | ||
# | ||
# #>pdf-composer100 | ||
# composer.container do |container| | ||
# container.box(:base, height: 20, style: {background_color: "hp-blue-dark"}) | ||
# container.box(:base, height: 20, style: {background_color: "hp-blue"}) | ||
# container.box(:base, height: 20, style: {background_color: "hp-blue-light"}) | ||
# end | ||
# | ||
# * The bottom to top layout (using valign = :bottom to fill up from the bottom and mask_mode = | ||
# :fill_horizontal to only remove the area to the left and right of the box): | ||
# | ||
# #>pdf-composer100 | ||
# composer.container do |container| | ||
# container.box(:base, height: 20, style: {background_color: "hp-blue-dark", | ||
# mask_mode: :fill_horizontal, valign: :bottom}) | ||
# container.box(:base, height: 20, style: {background_color: "hp-blue", | ||
# mask_mode: :fill_horizontal, valign: :bottom}) | ||
# container.box(:base, height: 20, style: {background_color: "hp-blue-light", | ||
# mask_mode: :fill_horizontal, valign: :bottom}) | ||
# end | ||
# | ||
# * The left to right layout (using mask_mode = :fill_vertical to fill the area to the top and | ||
# bottom of the box): | ||
# | ||
# #>pdf-composer100 | ||
# composer.container do |container| | ||
# container.box(:base, width: 20, style: {background_color: "hp-blue-dark", | ||
# mask_mode: :fill_vertical}) | ||
# container.box(:base, width: 20, style: {background_color: "hp-blue", | ||
# mask_mode: :fill_vertical}) | ||
# container.box(:base, width: 20, style: {background_color: "hp-blue-light", | ||
# mask_mode: :fill_vertical}) | ||
# end | ||
# | ||
# * The right to left layout (using align = :right to fill up from the right and mask_mode = | ||
# :fill_vertical to fill the area to the top and bottom of the box): | ||
# | ||
# #>pdf-composer100 | ||
# composer.container do |container| | ||
# container.box(:base, width: 20, style: {background_color: "hp-blue-dark", | ||
# mask_mode: :fill_vertical, align: :right}) | ||
# container.box(:base, width: 20, style: {background_color: "hp-blue", | ||
# mask_mode: :fill_vertical, align: :right}) | ||
# container.box(:base, width: 20, style: {background_color: "hp-blue-light", | ||
# mask_mode: :fill_vertical, align: :right}) | ||
# end | ||
class ContainerBox < Box | ||
|
||
# The child boxes of this ContainerBox. They need to be finalized before #fit is called. | ||
attr_reader :children | ||
|
||
# Creates a new container box, optionally accepting an array of child boxes. | ||
# | ||
# Example: | ||
# | ||
# #>pdf-composer100 | ||
# composer.text("A paragraph here") | ||
# composer.container(height: 40, style: {border: {width: 1}, padding: 5, | ||
# align: :center}) do |container| | ||
# container.text("Some", mask_mode: :fill_vertical) | ||
# container.text("text", mask_mode: :fill_vertical, valign: :center) | ||
# container.text("here", mask_mode: :fill_vertical, valign: :bottom) | ||
# end | ||
# composer.text("Another paragraph") | ||
def initialize(children: [], **kwargs) | ||
super(**kwargs) | ||
@children = children | ||
end | ||
|
||
# Returns +true+ if no box was fitted into the container. | ||
def empty? | ||
super && (!@box_fitter || @box_fitter.fit_results.empty?) | ||
end | ||
|
||
private | ||
|
||
# Fits the children into the container. | ||
def fit_content(available_width, available_height, frame) | ||
my_frame = Frame.new(frame.x + reserved_width_left, frame.y - @height + reserved_height_bottom, | ||
content_width, content_height, context: frame.context) | ||
@box_fitter = BoxFitter.new([my_frame]) | ||
children.each {|box| @box_fitter.fit(box) } | ||
|
||
if @box_fitter.fit_successful? | ||
update_content_width do | ||
result = @box_fitter.fit_results.max_by {|result| result.mask.x + result.mask.width } | ||
children.size > 0 ? result.mask.x + result.mask.width - my_frame.left : 0 | ||
end | ||
update_content_height { @box_fitter.content_heights.max } | ||
true | ||
end | ||
end | ||
|
||
# Draws the image onto the canvas at position [x, y]. | ||
def draw_content(canvas, x, y) | ||
dx = x - @fit_x | ||
dy = y - @fit_y | ||
@box_fitter.fit_results.each {|result| result.draw(canvas, dx: dx, dy: dy) } | ||
end | ||
|
||
end | ||
|
||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
# -*- encoding: utf-8 -*- | ||
|
||
require 'test_helper' | ||
require 'hexapdf/document' | ||
require 'hexapdf/layout/container_box' | ||
|
||
describe HexaPDF::Layout::ContainerBox do | ||
before do | ||
@doc = HexaPDF::Document.new | ||
@frame = HexaPDF::Layout::Frame.new(0, 0, 100, 100) | ||
end | ||
|
||
def create_box(children, **kwargs) | ||
HexaPDF::Layout::ContainerBox.new(children: Array(children), **kwargs) | ||
end | ||
|
||
def check_box(box, width, height, fit_pos = nil) | ||
assert(box.fit(@frame.available_width, @frame.available_height, @frame), "box didn't fit") | ||
assert_equal(width, box.width, "box width") | ||
assert_equal(height, box.height, "box height") | ||
if fit_pos | ||
box_fitter = box.instance_variable_get(:@box_fitter) | ||
assert_equal(fit_pos.size, box_fitter.fit_results.size) | ||
fit_pos.each_with_index do |(x, y), index| | ||
assert_equal(x, box_fitter.fit_results[index].x, "result[#{index}].x") | ||
assert_equal(y, box_fitter.fit_results[index].y, "result[#{index}].y") | ||
end | ||
end | ||
end | ||
|
||
describe "empty?" do | ||
it "is empty if nothing is fit yet" do | ||
assert(create_box([]).empty?) | ||
end | ||
|
||
it "is empty if no box fits" do | ||
box = create_box(@doc.layout.box(width: 110)) | ||
box.fit(@frame.available_width, @frame.available_height, @frame) | ||
assert(box.empty?) | ||
end | ||
|
||
it "is not empty if at least one box fits" do | ||
box = create_box(@doc.layout.box(width: 50, height: 20)) | ||
check_box(box, 100, 20) | ||
refute(box.empty?) | ||
end | ||
end | ||
|
||
describe "fit_content" do | ||
it "fits the children according to their mask_mode, valign, and align style properties" do | ||
box = create_box([@doc.layout.box(height: 20), | ||
@doc.layout.box(height: 20, style: {valign: :bottom, mask_mode: :fill_horizontal}), | ||
@doc.layout.box(width: 20, style: {align: :right, mask_mode: :fill_vertical})]) | ||
check_box(box, 100, 100, [[0, 80], [0, 0], [80, 20]]) | ||
end | ||
|
||
it "respects the initially set width/height as well as border/padding" do | ||
box = create_box(@doc.layout.box(height: 20), height: 50, width: 40, | ||
style: {padding: 2, border: {width: 3}}) | ||
check_box(box, 40, 50, [[5, 75]]) | ||
end | ||
end | ||
|
||
describe "draw_content" do | ||
before do | ||
@canvas = @doc.pages.add.canvas | ||
end | ||
|
||
it "draws the result onto the canvas" do | ||
child_box = @doc.layout.box(height: 20) do |canvas, b| | ||
canvas.line(0, 0, b.content_width, b.content_height).end_path | ||
end | ||
box = create_box(child_box) | ||
box.fit(100, 100, @frame) | ||
box.draw(@canvas, 0, 50) | ||
assert_operators(@canvas.contents, [[:save_graphics_state], | ||
[:concatenate_matrix, [1, 0, 0, 1, 0, 50]], | ||
[:move_to, [0, 0]], | ||
[:line_to, [100, 20]], | ||
[:end_path], | ||
[:restore_graphics_state]]) | ||
end | ||
end | ||
end |