# -*- 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-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):
# 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 .
#
# 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 .
#++
require 'hexapdf/layout/box'
module HexaPDF
module Layout
# A ColumnBox arranges boxes in one or more columns.
#
# The number of columns as well as the size of the gap between the columns can be modified.
class ColumnBox < Box
# The child boxes of this ColumnBox.
attr_reader :children
# The number of columns.
# TODO: allow array with column widths later like [100, :*, :*]; same for gaps
attr_reader :columns
# The size of the gap between the columns.
attr_reader :gap
# Creates a new ColumnBox object for the given +children+ boxes.
def initialize(children = [], columns = 2, gap: 36, **kwargs)
super(**kwargs)
@children = children
@columns = columns
@gap = gap
end
# Fits the column box into the available space.
def fit(available_width, available_height, frame)
last_height_difference = 1_000_000
height = if style.position == :flow
frame.height
else
(@initial_height > 0 ? @initial_height : available_height) - reserved_height
end
while true
p '-'*100
@frames = []
if style.position == :flow
column_width = (frame.width - gap * (@columns - 1)).to_f / @columns
@columns.times do |col_nr|
left = (column_width + gap) * col_nr + frame.left
bottom = frame.bottom
rect = Geom2D::Polygon([left, bottom],
[left + column_width, bottom],
[left + column_width, bottom + height],
[left, bottom + height])
shape = Geom2D::Algorithms::PolygonOperation.run(frame.shape, rect, :intersection)
col_frame = Frame.new(left, bottom, column_width, height)
col_frame.shape = shape
@frames << col_frame
end
@frame_index = 0
@results = @children.map {|child_box| fit_box(child_box) }
@width = frame.width
@height = frame.height - @frames.min_by(&:y).y
else
width = (@initial_width > 0 ? @initial_width : available_width) - reserved_width
column_width = (width - gap * (@columns - 1)).to_f / @columns
@columns.times do |col_nr|
@frames << Frame.new((column_width + gap) * col_nr, 0, column_width, height)
end
@frame_index = 0
@results = @children.map {|child_box| fit_box(child_box) }
@width = width
@height = height - @frames.min_by(&:y).y
end
min_y, max_y = @frames.minmax_by(&:y).map(&:y)
p [height, @frames.map(&:y), last_height_difference, min_y, max_y]
# TOOD: @result.any?(&:empty?) only for the first run!!!! if the first run fails, we
# cannot balance the columns because there is too much content.
# TODO: another break condition is if the @results didn't change since the last run
p [:maybe_redo, min_y, max_y, height, last_height_difference]
p [@results.map {|arr| arr.all? {|r| r.status }}]
break if max_y != height && @results.all? {|arr| !arr.empty? && arr.all? {|r| r.success? }} &&
(@results.any?(&:empty?) ||
max_y - min_y >= last_height_difference ||
max_y - min_y < 0.5)
if max_y == 0 && min_y == 0
height += last_height_difference / 4.0
else
last_height_difference = max_y - min_y
height -= last_height_difference / 2.0
end
end
@results.all? {|res| res.length == 1 }
end
private
def fit_box(box)
cur_frame = @frames[@frame_index]
fit_results = []
while cur_frame
result = cur_frame.fit(box)
if result.success?
cur_frame.remove_area(result.mask)
fit_results << result
break
elsif cur_frame.full?
@frame_index += 1
break if @frame_index == @frames.length
cur_frame = @frames[@frame_index]
else
draw_box, box = cur_frame.split(result)
if draw_box
cur_frame.remove_area(result.mask)
fit_results << result
elsif !cur_frame.find_next_region
@frame_index += 1
break if @frame_index == @frames.length
cur_frame = @frames[@frame_index]
end
end
end
fit_results
end
# Draws the child boxes onto the canvas at position [x, y].
def draw_content(canvas, x, y)
x = y = 0 if style.position == :flow
@results.each do |result_boxes|
result_boxes.each do |result|
result.box.draw(canvas, x + result.x, y + result.y)
end
end
end
end
end
end