1
1
mirror of https://github.com/Kozea/WeasyPrint.git synced 2024-10-05 16:37:47 +03:00

Handle flex-direction

This commit is contained in:
Guillaume Ayoub 2018-01-21 12:49:00 +01:00
parent fef83a2513
commit e498cbcbd4

View File

@ -15,6 +15,7 @@ from __future__ import division, unicode_literals
from math import log10
from ..css.properties import Dimension
from ..formatting_structure import boxes
from .percentages import resolve_percentages
from .preferred import max_content_width, min_content_width
from .tables import find_in_flow_baseline
@ -32,41 +33,73 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block,
# Step 1 is done in formatting_structure.boxes
# Step 2
if box.style.width != 'auto':
available_main_space = box.style.width.value
if box.style['flex_direction'].startswith('row'):
axis, cross = 'width', 'height'
else:
available_main_space = (
containing_block.width - (box.margin_left - box.margin_right) -
(box.padding_left - box.padding_right) -
(box.border_left_width - box.border_right_width))
axis, cross = 'height', 'width'
if box.style.height != 'auto':
available_cross_space = box.style.height.value
if box.style[axis] != 'auto':
available_main_space = box.style[axis].value
else:
available_cross_space = (
max_position_y - box.position_y -
(box.margin_top - box.margin_bottom) -
(box.padding_top - box.padding_bottom) -
(box.border_top_width - box.border_bottom_width))
if axis == 'width':
available_main_space = (
containing_block.width -
box.margin_left - box.margin_right -
box.padding_left - box.padding_right -
box.border_left_width - box.border_right_width)
else:
main_space = max_position_y - box.content_box_y()
if containing_block.height != 'auto':
assert containing_block.height.unit == 'px'
main_space = min(main_space, containing_block.height.value)
available_main_space = (
main_space -
box.margin_top - box.margin_bottom -
box.padding_top - box.padding_bottom -
box.border_top_width - box.border_bottom_width)
if box.style[cross] != 'auto':
available_cross_space = box.style[cross].value
else:
if cross == 'height':
main_space = max_position_y - box.content_box_y()
if containing_block.height != 'auto':
assert containing_block.height.unit == 'px'
main_space = min(main_space, containing_block.height.value)
available_cross_space = (
main_space -
box.margin_top - box.margin_bottom -
box.padding_top - box.padding_bottom -
box.border_top_width - box.border_bottom_width)
else:
available_cross_space = (
containing_block.width -
box.margin_left - box.margin_right -
box.padding_left - box.padding_right -
box.border_left_width - box.border_right_width)
# Step 3
for child in box.children:
if not child.is_flex_item:
continue
resolve_percentages(child, (0, 0))
child.position_x = box.content_box_x()
child.position_y = box.content_box_y()
child.sytle = child.style.copy()
resolve_percentages(box, containing_block)
flex_basis = child.style['flex_basis']
# "If a value would resolve to auto for width, it instead resolves
# to content for flex-basis."
# to content for flex-basis." Let's do this for height too.
# See https://www.w3.org/TR/css-flexbox-1/#propdef-flex-basis
if flex_basis == 'auto':
if child.style['width'] == 'auto':
if child.style[axis] == 'auto':
flex_basis = 'content'
else:
flex_basis = child.style['width']
flex_basis = child.style[axis]
# Step 3.A
# TODO: handle percentages
@ -82,47 +115,80 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block,
# Step 3.E
else:
if flex_basis == 'content':
child.style['width'] = 'max-content'
child.style[axis] = 'max-content'
else:
child.style['width'] = flex_basis
child.style[axis] = flex_basis
# TODO: don't set style width, support *-content values instead
if child.style['width'] == 'max-content':
child.style['width'] = 'auto'
child.flex_base_size = max_content_width(context, child)
elif child.style['width'] == 'min-content':
child.style['width'] = 'auto'
child.flex_base_size = min_content_width(context, child)
# TODO: don't set style value, support *-content values instead
if child.style[axis] == 'max-content':
child.style[axis] = 'auto'
if axis == 'width':
child.flex_base_size = max_content_width(context, child)
else:
new_child = child.copy_with_children(child.children)
new_child.width = float('inf')
new_child = blocks.block_container_layout(
context, new_child, float('inf'), skip_stack,
device_size, page_is_empty, absolute_boxes,
fixed_boxes)[0]
child.flex_base_size = new_child.height
elif child.style[axis] == 'min-content':
child.style[axis] = 'auto'
if axis == 'width':
child.flex_base_size = min_content_width(context, child)
else:
new_child = child.copy_with_children(child.children)
new_child.width = 0
new_child = blocks.block_container_layout(
context, new_child, float('inf'), skip_stack,
device_size, page_is_empty, absolute_boxes,
fixed_boxes)[0]
child.flex_base_size = new_child.height
else:
assert child.style['width'].unit == 'px'
child.flex_base_size = child.style['width'].value
assert child.style[axis].unit == 'px'
child.flex_base_size = child.style[axis].value
# TODO: the flex base size shouldn't take care of min and max sizes
child.hypothetical_main_size = child.flex_base_size
# Step 4
# TODO: don't use block_level_width, see TODO in build.flex_children.
blocks.block_level_width(box, containing_block)
if axis == 'width':
blocks.block_level_width(box, containing_block)
else:
if box.style['height'] != 'auto':
box.height = box.style['height'].value
else:
box.height = 0
for child in box.children:
box.height += (
child.hypothetical_main_size +
child.border_top_width + child.border_bottom_width +
child.padding_top + child.padding_bottom)
if child.margin_top != 'auto':
box.height += child.margin_top
if child.margin_bottom != 'auto':
box.height += child.margin_bottom
# Step 5
flex_lines = []
line = []
line_width = 0
line_size = 0
axis_size = getattr(box, axis)
for child in sorted(box.children, key=lambda item: item.style['order']):
if not child.is_flex_item:
continue
line_width += child.hypothetical_main_size
if box.style['flex_wrap'] != 'nowrap' and line_width > box.width:
line_size += child.hypothetical_main_size
if box.style['flex_wrap'] != 'nowrap' and line_size > axis_size:
if line:
flex_lines.append(FlexLine(line))
line = [child]
line_width = child.hypothetical_main_size
line_size = child.hypothetical_main_size
else:
line.append(child)
flex_lines.append(FlexLine(line))
line = []
line_width = 0
line_size = 0
else:
line.append(child)
if line:
@ -160,8 +226,6 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block,
child.target_main_size = child.hypothetical_main_size
child.frozen = True
else:
# TODO: do we need to set target main size here?
child.target_main_size = child.hypothetical_main_size
child.frozen = False
# Step 6.3
@ -198,7 +262,13 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block,
remaining_free_space = initial_free_space
# Step 6.4.c
if remaining_free_space != 0:
if remaining_free_space == 0:
# "Do nothing", but we at least set the flex_base_size as
# target_main_size for next step.
for child in line:
if not child.frozen:
child.target_main_size = child.flex_base_size
else:
scaled_flex_shrink_factors_sum = 0
for child in line:
if not child.frozen:
@ -242,40 +312,40 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block,
child.frozen = True
# Step 6.5
# TODO: don't set style width, find a way to avoid width
# re-calculation in Step 7
for child in line:
child.style['width'] = Dimension(child.target_main_size, 'px')
setattr(child, axis, child.target_main_size)
# Step 7
for line in flex_lines:
for child in line:
child.position_x = box.content_box_x()
child.position_y = box.content_box_y()
# TODO: don't use block_level_layout, see TODOs in Step 6.5 and
# build.flex_children.
flex_lines = [FlexLine(
blocks.block_level_layout(
context, child, max_position_y, skip_stack, box, device_size,
page_is_empty, absolute_boxes, fixed_boxes,
adjoining_margins=[])[0]
for child in line) for line in flex_lines]
# TODO: fix TODO in build.flex_children
# TODO: Handle breaks, skip_stack and resume_at instead of excluding Nones
flex_lines = [
FlexLine([child for child in line if child is not None])
for line in flex_lines]
flex_lines = [line for line in flex_lines if line]
new_flex_lines = []
for line in flex_lines:
new_flex_line = FlexLine()
for child in line:
if cross == 'height':
child = blocks.block_container_layout(
context, child,
available_cross_space + box.content_box_y(), skip_stack,
device_size, page_is_empty, absolute_boxes, fixed_boxes)[0]
else:
child.width = min_content_width(context, child, outer=False)
if child is not None:
new_flex_line.append(child)
if new_flex_line:
new_flex_lines.append(new_flex_line)
flex_lines = new_flex_lines
# Step 8
if len(flex_lines) == 1 and box.height != 'auto':
flex_lines[0].height = box.height
cross_size = getattr(box, cross)
if len(flex_lines) == 1 and cross_size != 'auto':
flex_lines[0].cross_size = cross_size
else:
for line in flex_lines:
collected_items = []
not_collected_items = []
for child in line:
align_self = 'baseline'
if (box.style['flex_direction'] == 'row' and
align_self = child.style['align_self']
if (box.style['flex_direction'].startswith('row') and
align_self == 'baseline' and
child.margin_top != 'auto' and
child.margin_bottom != 'auto'):
@ -293,21 +363,34 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block,
cross_start_distance = max(cross_start_distance, baseline)
cross_end_distance = max(
cross_end_distance, child.margin_height() - baseline)
collected_height = cross_start_distance + cross_end_distance
non_collected_height = 0
collected_cross_size = cross_start_distance + cross_end_distance
non_collected_cross_size = 0
if not_collected_items:
non_collected_height = max(
child.margin_height() for child in not_collected_items)
line.height = max(collected_height, non_collected_height)
non_collected_cross_size = max(
child.margin_height() if cross == 'height'
else child.margin_width()
for child in not_collected_items)
line.cross_size = max(
collected_cross_size, non_collected_cross_size)
# TODO: handle min/max height for single-line containers
# Step 9
if box.style['height'] != 'auto':
if box.style['align_content'] == 'stretch':
extra_height = box.height - sum(line.height for line in flex_lines)
if extra_height:
if box.style['align_content'] == 'stretch':
definite_cross_size = None
if cross == 'height' and box.style['height'] != 'auto':
definite_cross_size = box.style['height'].value
elif cross == 'width':
if isinstance(box, boxes.FlexBox):
if box.style['width'] == 'auto':
definite_cross_size = available_cross_space
else:
definite_cross_size = box.style['width'].value
if definite_cross_size is not None:
extra_cross_size = definite_cross_size - sum(
line.cross_size for line in flex_lines)
if extra_cross_size:
for line in flex_lines:
line.height += extra_height / len(flex_lines)
line.cross_size += extra_cross_size / len(flex_lines)
# TODO: Step 10
@ -317,55 +400,101 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block,
align_self = child.style['align_self']
if align_self == 'auto':
align_self = box.style['align_items']
if align_self == 'stretch' and child.style['height'] == 'auto':
if 'auto' not in (child.margin_top, child.margin_bottom):
child.height = line.height
# TODO: redo layout
if align_self == 'stretch' and child.style[cross] == 'auto':
cross_margins = (
(child.margin_top, child.margin_bottom)
if cross == 'height'
else (child.margin_left, child.margin_right))
if child.style[cross] == 'auto':
if 'auto' not in cross_margins:
cross_size = line.cross_size
if cross == 'height':
cross_size -= (
child.margin_top + child.margin_bottom +
child.padding_top + child.padding_bottom +
child.border_top_width +
child.border_bottom_width)
else:
cross_size -= (
child.margin_left + child.margin_right +
child.padding_left + child.padding_right +
child.border_left_width +
child.border_right_width)
setattr(child, cross, cross_size)
# TODO: redo layout?
# else: Cross size has been set by step 7
# Step 12
free_space = available_main_space - sum(
child.margin_width() for line in flex_lines for child in line)
if free_space:
margins = 0
for line in flex_lines:
for child in line:
if child.margin_left == 'auto':
margins += 1
if child.margin_right == 'auto':
margins += 1
if margins:
free_space /= margins
for line in flex_lines:
for child in line:
if child.margin_left == 'auto':
child.margin_left = free_space
if child.margin_right == 'auto':
child.margin_right = free_space
free_space = 0
# TODO: handle rtl
original_position_axis = (
box.content_box_x() if axis == 'width'
else box.content_box_y())
justify_content = box.style['justify_content']
if box.style['flex_direction'].endswith('-reverse'):
if justify_content == 'flex-start':
justify_content = 'flex-end'
elif justify_content == 'flex-end':
justify_content = 'flex-start'
for line in flex_lines:
# TODO: handle rtl
position_x = box.content_box_x()
if box.style['justify_content'] == 'flex-end':
position_x += free_space
elif box.style['justify_content'] == 'center':
position_x += free_space / 2
elif box.style['justify_content'] == 'space-around':
position_x += free_space / len(line) / 2
position_axis = original_position_axis
free_space = getattr(box, axis) - sum(
child.margin_width() if axis == 'width'
else child.margin_height() for child in line)
if free_space > 0:
margins = 0
for child in line:
if axis == 'width':
if child.margin_left == 'auto':
margins += 1
if child.margin_right == 'auto':
margins += 1
else:
if child.margin_top == 'auto':
margins += 1
if child.margin_bottom == 'auto':
margins += 1
if margins:
free_space /= margins
for child in line:
if axis == 'width':
if child.margin_left == 'auto':
child.margin_left = free_space
if child.margin_right == 'auto':
child.margin_right = free_space
else:
if child.margin_top == 'auto':
child.margin_top = free_space
if child.margin_bottom == 'auto':
child.margin_bottom = free_space
free_space = 0
if justify_content == 'flex-end':
position_axis += free_space
elif justify_content == 'center':
position_axis += free_space / 2
elif justify_content == 'space-around':
position_axis += free_space / len(line) / 2
for child in line:
child.position_x = position_x
position_x += child.margin_width()
if box.style['justify_content'] == 'space-around':
position_x += free_space / len(line)
elif box.style['justify_content'] == 'space-between':
if axis == 'width':
child.position_x = position_axis
else:
child.position_y = position_axis
position_axis += (
child.margin_width() if axis == 'width'
else child.margin_height())
if justify_content == 'space-around':
position_axis += free_space / len(line)
elif justify_content == 'space-between':
if len(line) > 1:
position_x += free_space / (len(line) - 1)
position_axis += free_space / (len(line) - 1)
# Step 13
position_y = box.content_box_y()
position_cross = (
box.content_box_y() if cross == 'height'
else box.content_box_x())
for line in flex_lines:
lower_baseline = 0
# TODO: don't duplicate this loop
@ -373,7 +502,8 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block,
align_self = child.style['align_self']
if align_self == 'auto':
align_self = box.style['align_items']
if align_self == 'baseline':
if align_self == 'baseline' and axis == 'width':
# TODO: handle vertical text
baseline = find_in_flow_baseline(child)
if baseline is None:
# TODO: "If the item does not have a baseline in the
@ -381,84 +511,128 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block,
# items border box."
child.baseline = 0
else:
child.baseline = baseline - position_y
child.baseline = baseline - position_cross
lower_baseline = max(lower_baseline, child.baseline)
for child in line:
auto_margins = sum([
margin == 'auto' for margin in (
child.margin_top, child.margin_bottom)])
cross_margins = (
(child.margin_top, child.margin_bottom) if cross == 'height'
else (child.margin_left, child.margin_right))
auto_margins = sum([margin == 'auto' for margin in cross_margins])
if auto_margins:
# TODO: take care of margins insead of using margin height
extra_height = available_cross_space - child.margin_height()
if extra_height > 0:
extra_height /= auto_margins
if child.margin_top == 'auto':
child.margin_top = extra_height
if child.margin_bottom == 'auto':
child.margin_bottom = extra_height
# TODO: take care of margins insead of using margin_*()
extra_cross = available_cross_space - (
child.margin_height() if cross == 'height'
else child.margin_width())
if extra_cross > 0:
extra_cross /= auto_margins
if cross == 'height':
if child.margin_top == 'auto':
child.margin_top = extra_cross
if child.margin_bottom == 'auto':
child.margin_bottom = extra_cross
else:
if child.margin_left == 'auto':
child.margin_left = extra_cross
if child.margin_right == 'auto':
child.margin_right = extra_cross
else:
if child.margin_top == 'auto':
child.margin_top = 0
child.margin_bottom = extra_height
if cross == 'height':
if child.margin_top == 'auto':
child.margin_top = 0
child.margin_bottom = extra_cross
else:
if child.margin_left == 'auto':
child.margin_left = 0
child.margin_right = extra_cross
else:
# Step 14
align_self = child.style['align_self']
if align_self == 'auto':
align_self = box.style['align_items']
child.position_y = position_y
position = 'position_y' if cross == 'height' else 'position_x'
setattr(child, position, position_cross)
if align_self == 'flex-end':
child.position_y += line.height - child.margin_height()
if cross == 'height':
child.position_y += (
line.cross_size - child.margin_height())
else:
child.position_x += (
line.cross_size - child.margin_width())
elif align_self == 'center':
child.position_y += (
line.height - child.margin_height()) / 2
if cross == 'height':
child.position_y += (
line.cross_size - child.margin_height()) / 2
else:
child.position_x += (
line.cross_size - child.margin_width()) / 2
elif align_self == 'baseline':
child.position_y += lower_baseline - child.baseline
if cross == 'height':
child.position_y += lower_baseline - child.baseline
else:
# Handle vertical text
pass
elif align_self == 'stretch':
# TODO: don't set style width, find a way to avoid width
# re-calculation after Step 16
# TODO: take care of box-sizing
margins = (
child.margin_top + child.margin_bottom +
child.border_top_width + child.border_bottom_width +
child.padding_top + child.padding_bottom)
child.style['height'] = Dimension(
line.height - margins, 'px')
position_y += line.height
if child.style[cross] == 'auto':
if cross == 'height':
margins = (
child.margin_top + child.margin_bottom +
child.border_top_width +
child.border_bottom_width +
child.padding_top + child.padding_bottom)
else:
margins = (
child.margin_left + child.margin_right +
child.border_left_width +
child.border_right_width +
child.padding_left + child.padding_right)
# TODO: don't set style width, find a way to avoid
# width re-calculation after Step 16
child.style[cross] = Dimension(
line.cross_size - margins, 'px')
position_cross += line.cross_size
# Step 15
if box.style['height'] == 'auto':
if box.style[cross] == 'auto':
# TODO: handle min-max
box.height = sum(line.height for line in flex_lines)
setattr(box, cross, sum(line.cross_size for line in flex_lines))
# Step 16
elif len(flex_lines) > 1:
extra_height = box.height - sum(line.height for line in flex_lines)
if extra_height > 0:
y_translate = 0
extra_cross_size = getattr(box, cross) - sum(
line.cross_size for line in flex_lines)
direction = 'dy' if cross == 'height' else 'dx'
if extra_cross_size > 0:
cross_translate = 0
for line in flex_lines:
for child in line:
if child.is_flex_item:
child.translate(dy=y_translate)
child.translate(**{direction: cross_translate})
if box.style['align_content'] == 'flex-end':
child.translate(dy=extra_height)
child.translate(**{direction: extra_cross_size})
elif box.style['align_content'] == 'center':
child.translate(dy=extra_height / 2)
elif box.style['align_content'] == 'space-around':
child.translate(
dy=extra_height / len(flex_lines) / 2)
**{direction: extra_cross_size / 2})
elif box.style['align_content'] == 'space-around':
child.translate(**{
direction: extra_cross_size /
len(flex_lines) / 2})
if box.style['align_content'] == 'space-between':
y_translate += extra_height / (len(flex_lines) - 1)
cross_translate += extra_cross_size / (len(flex_lines) - 1)
elif box.style['align_content'] == 'space-around':
y_translate += extra_height / len(flex_lines)
cross_translate += extra_cross_size / len(flex_lines)
# TODO: don't use block_level_layout, see TODOs in Step 14 and
# build.flex_children.
box.children = [
blocks.block_level_layout(
context, child, max_position_y, skip_stack, box, device_size,
page_is_empty, absolute_boxes, fixed_boxes,
adjoining_margins=[])[0]
for line in flex_lines for child in line if child.is_flex_item]
box.children = []
for line in flex_lines:
for child in line:
if child.is_flex_item:
new_child = blocks.block_container_layout(
context, child, max_position_y, skip_stack, device_size,
page_is_empty, absolute_boxes, fixed_boxes)[0]
if new_child is not None:
box.children.append(new_child)
# TODO: check these returned values
return box, None, {'break': 'any', 'page': None}, [], False