You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
623 lines
18 KiB
623 lines
18 KiB
/*
|
|
* Copyright 2023 Vincent Sanders <vince@netsurf-browser.org>
|
|
*
|
|
* This file is part of NetSurf, http://www.netsurf-browser.org/
|
|
*
|
|
* NetSurf is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; version 2 of the License.
|
|
*
|
|
* NetSurf 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 General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
/**
|
|
* \file
|
|
* HTML layout implementation for tables
|
|
*/
|
|
|
|
#include <stdbool.h>
|
|
#include <string.h>
|
|
#include <math.h>
|
|
|
|
#include "utils/log.h"
|
|
#include "utils/utils.h"
|
|
#include "netsurf/types.h"
|
|
#include "netsurf/mouse.h"
|
|
#include "desktop/scrollbar.h"
|
|
|
|
#include "html/private.h"
|
|
#include "html/box.h"
|
|
#include "html/table.h"
|
|
|
|
#include "html/layout/internal.h"
|
|
#include "html/layout/block.h"
|
|
#include "html/layout/table.h"
|
|
|
|
/**
|
|
* Moves the children of a box by a specified amount
|
|
*
|
|
* \param box top of tree of boxes
|
|
* \param x the amount to move children by horizontally
|
|
* \param y the amount to move children by vertically
|
|
*/
|
|
static void layout_move_children(struct box *box, int x, int y)
|
|
{
|
|
assert(box);
|
|
|
|
for (box = box->children; box; box = box->next) {
|
|
box->x += x;
|
|
box->y += y;
|
|
}
|
|
}
|
|
|
|
/* Documented in layout_table.h */
|
|
bool layout_table(struct box *table, int available_width, html_content *content)
|
|
{
|
|
unsigned int columns = table->columns; /* total columns */
|
|
unsigned int i;
|
|
unsigned int *row_span;
|
|
int *excess_y;
|
|
int table_width, min_width = 0, max_width = 0;
|
|
int required_width = 0;
|
|
int x, remainder = 0, count = 0;
|
|
int table_height = 0;
|
|
int min_height = 0;
|
|
int *xs; /* array of column x positions */
|
|
int auto_width;
|
|
int spare_width;
|
|
int relative_sum = 0;
|
|
int border_spacing_h = 0, border_spacing_v = 0;
|
|
int spare_height;
|
|
int positioned_columns = 0;
|
|
struct box *containing_block = NULL;
|
|
struct box *c;
|
|
struct box *row;
|
|
struct box *row_group;
|
|
struct box **row_span_cell;
|
|
struct column *col;
|
|
const css_computed_style *style = table->style;
|
|
enum css_width_e wtype;
|
|
enum css_height_e htype;
|
|
css_fixed value = 0;
|
|
css_unit unit = CSS_UNIT_PX;
|
|
|
|
assert(table->type == BOX_TABLE);
|
|
assert(style);
|
|
assert(table->children && table->children->children);
|
|
assert(columns);
|
|
|
|
/* allocate working buffers */
|
|
col = malloc(columns * sizeof col[0]);
|
|
excess_y = malloc(columns * sizeof excess_y[0]);
|
|
row_span = malloc(columns * sizeof row_span[0]);
|
|
row_span_cell = malloc(columns * sizeof row_span_cell[0]);
|
|
xs = malloc((columns + 1) * sizeof xs[0]);
|
|
if (!col || !xs || !row_span || !excess_y || !row_span_cell) {
|
|
free(col);
|
|
free(excess_y);
|
|
free(row_span);
|
|
free(row_span_cell);
|
|
free(xs);
|
|
return false;
|
|
}
|
|
|
|
memcpy(col, table->col, sizeof(col[0]) * columns);
|
|
|
|
/* find margins, paddings, and borders for table and cells */
|
|
layout_find_dimensions(&content->unit_len_ctx, available_width, -1, table,
|
|
style, 0, 0, 0, 0, 0, 0, table->margin, table->padding,
|
|
table->border);
|
|
for (row_group = table->children; row_group;
|
|
row_group = row_group->next) {
|
|
for (row = row_group->children; row; row = row->next) {
|
|
for (c = row->children; c; c = c->next) {
|
|
enum css_overflow_e overflow_x;
|
|
enum css_overflow_e overflow_y;
|
|
|
|
assert(c->style);
|
|
table_used_border_for_cell(
|
|
&content->unit_len_ctx, c);
|
|
layout_find_dimensions(&content->unit_len_ctx,
|
|
available_width, -1, c,
|
|
c->style, 0, 0, 0, 0, 0, 0,
|
|
0, c->padding, c->border);
|
|
|
|
overflow_x = css_computed_overflow_x(c->style);
|
|
overflow_y = css_computed_overflow_y(c->style);
|
|
|
|
if (overflow_x == CSS_OVERFLOW_SCROLL ||
|
|
overflow_x ==
|
|
CSS_OVERFLOW_AUTO) {
|
|
c->padding[BOTTOM] += SCROLLBAR_WIDTH;
|
|
}
|
|
if (overflow_y == CSS_OVERFLOW_SCROLL ||
|
|
overflow_y ==
|
|
CSS_OVERFLOW_AUTO) {
|
|
c->padding[RIGHT] += SCROLLBAR_WIDTH;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* border-spacing is used in the separated borders model */
|
|
if (css_computed_border_collapse(style) ==
|
|
CSS_BORDER_COLLAPSE_SEPARATE) {
|
|
css_fixed h = 0, v = 0;
|
|
css_unit hu = CSS_UNIT_PX, vu = CSS_UNIT_PX;
|
|
|
|
css_computed_border_spacing(style, &h, &hu, &v, &vu);
|
|
|
|
border_spacing_h = FIXTOINT(css_unit_len2device_px(
|
|
style, &content->unit_len_ctx, h, hu));
|
|
border_spacing_v = FIXTOINT(css_unit_len2device_px(
|
|
style, &content->unit_len_ctx, v, vu));
|
|
}
|
|
|
|
/* find specified table width, or available width if auto-width */
|
|
wtype = css_computed_width(style, &value, &unit);
|
|
if (wtype == CSS_WIDTH_SET) {
|
|
if (unit == CSS_UNIT_PCT) {
|
|
table_width = FPCT_OF_INT_TOINT(value, available_width);
|
|
} else {
|
|
table_width =
|
|
FIXTOINT(css_unit_len2device_px(
|
|
style, &content->unit_len_ctx,
|
|
value, unit));
|
|
}
|
|
|
|
/* specified width includes border */
|
|
table_width -= table->border[LEFT].width +
|
|
table->border[RIGHT].width;
|
|
table_width = table_width < 0 ? 0 : table_width;
|
|
|
|
auto_width = table_width;
|
|
} else {
|
|
table_width = AUTO;
|
|
auto_width = available_width -
|
|
((table->margin[LEFT] == AUTO ? 0 :
|
|
table->margin[LEFT]) +
|
|
table->border[LEFT].width +
|
|
table->padding[LEFT] +
|
|
table->padding[RIGHT] +
|
|
table->border[RIGHT].width +
|
|
(table->margin[RIGHT] == AUTO ? 0 :
|
|
table->margin[RIGHT]));
|
|
}
|
|
|
|
/* Find any table height specified within CSS/HTML */
|
|
htype = css_computed_height(style, &value, &unit);
|
|
if (htype == CSS_HEIGHT_SET) {
|
|
if (unit == CSS_UNIT_PCT) {
|
|
/* This is the minimum height for the table
|
|
* (see 17.5.3) */
|
|
if (css_computed_position(table->style) ==
|
|
CSS_POSITION_ABSOLUTE) {
|
|
/* Table is absolutely positioned */
|
|
assert(table->float_container);
|
|
containing_block = table->float_container;
|
|
} else if (table->float_container &&
|
|
css_computed_position(table->style) !=
|
|
CSS_POSITION_ABSOLUTE &&
|
|
(css_computed_float(table->style) ==
|
|
CSS_FLOAT_LEFT ||
|
|
css_computed_float(table->style) ==
|
|
CSS_FLOAT_RIGHT)) {
|
|
/* Table is a float */
|
|
assert(table->parent && table->parent->parent &&
|
|
table->parent->parent->parent);
|
|
containing_block =
|
|
table->parent->parent->parent;
|
|
} else if (table->parent && table->parent->type !=
|
|
BOX_INLINE_CONTAINER) {
|
|
/* Table is a block level element */
|
|
containing_block = table->parent;
|
|
} else if (table->parent && table->parent->type ==
|
|
BOX_INLINE_CONTAINER) {
|
|
/* Table is an inline block */
|
|
assert(table->parent->parent);
|
|
containing_block = table->parent->parent;
|
|
}
|
|
|
|
if (containing_block) {
|
|
css_fixed ignored = 0;
|
|
|
|
htype = css_computed_height(
|
|
containing_block->style,
|
|
&ignored, &unit);
|
|
}
|
|
|
|
if (containing_block &&
|
|
containing_block->height != AUTO &&
|
|
(css_computed_position(table->style) ==
|
|
CSS_POSITION_ABSOLUTE ||
|
|
htype == CSS_HEIGHT_SET)) {
|
|
/* Table is absolutely positioned or its
|
|
* containing block has a valid specified
|
|
* height. (CSS 2.1 Section 10.5) */
|
|
min_height = FPCT_OF_INT_TOINT(value,
|
|
containing_block->height);
|
|
}
|
|
} else {
|
|
/* This is the minimum height for the table
|
|
* (see 17.5.3) */
|
|
min_height = FIXTOINT(css_unit_len2device_px(
|
|
style, &content->unit_len_ctx,
|
|
value, unit));
|
|
}
|
|
}
|
|
|
|
/* calculate width required by cells */
|
|
for (i = 0; i != columns; i++) {
|
|
|
|
NSLOG(layout, DEBUG,
|
|
"table %p, column %u: type %s, width %i, min %i, max %i",
|
|
table,
|
|
i,
|
|
((const char *[]){
|
|
"UNKNOWN",
|
|
"FIXED",
|
|
"AUTO",
|
|
"PERCENT",
|
|
"RELATIVE",
|
|
})[col[i].type],
|
|
col[i].width,
|
|
col[i].min,
|
|
col[i].max);
|
|
|
|
|
|
if (col[i].positioned) {
|
|
positioned_columns++;
|
|
continue;
|
|
} else if (col[i].type == COLUMN_WIDTH_FIXED) {
|
|
if (col[i].width < col[i].min)
|
|
col[i].width = col[i].max = col[i].min;
|
|
else
|
|
col[i].min = col[i].max = col[i].width;
|
|
required_width += col[i].width;
|
|
} else if (col[i].type == COLUMN_WIDTH_PERCENT) {
|
|
int width = col[i].width * auto_width / 100;
|
|
required_width += col[i].min < width ? width :
|
|
col[i].min;
|
|
} else
|
|
required_width += col[i].min;
|
|
|
|
NSLOG(layout, DEBUG, "required_width %i", required_width);
|
|
}
|
|
required_width += (columns + 1 - positioned_columns) *
|
|
border_spacing_h;
|
|
|
|
NSLOG(layout, DEBUG,
|
|
"width %i, min %i, max %i, auto %i, required %i", table_width,
|
|
table->min_width, table->max_width, auto_width, required_width);
|
|
|
|
if (auto_width < required_width) {
|
|
/* table narrower than required width for columns:
|
|
* treat percentage widths as maximums */
|
|
for (i = 0; i != columns; i++) {
|
|
if (col[i].type == COLUMN_WIDTH_RELATIVE)
|
|
continue;
|
|
if (col[i].type == COLUMN_WIDTH_PERCENT) {
|
|
col[i].max = auto_width * col[i].width / 100;
|
|
if (col[i].max < col[i].min)
|
|
col[i].max = col[i].min;
|
|
}
|
|
min_width += col[i].min;
|
|
max_width += col[i].max;
|
|
}
|
|
} else {
|
|
/* take percentages exactly */
|
|
for (i = 0; i != columns; i++) {
|
|
if (col[i].type == COLUMN_WIDTH_RELATIVE)
|
|
continue;
|
|
if (col[i].type == COLUMN_WIDTH_PERCENT) {
|
|
int width = auto_width * col[i].width / 100;
|
|
if (width < col[i].min)
|
|
width = col[i].min;
|
|
col[i].min = col[i].width = col[i].max = width;
|
|
col[i].type = COLUMN_WIDTH_FIXED;
|
|
}
|
|
min_width += col[i].min;
|
|
max_width += col[i].max;
|
|
}
|
|
}
|
|
|
|
/* allocate relative widths */
|
|
spare_width = auto_width;
|
|
for (i = 0; i != columns; i++) {
|
|
if (col[i].type == COLUMN_WIDTH_RELATIVE)
|
|
relative_sum += col[i].width;
|
|
else if (col[i].type == COLUMN_WIDTH_FIXED)
|
|
spare_width -= col[i].width;
|
|
else
|
|
spare_width -= col[i].min;
|
|
}
|
|
spare_width -= (columns + 1) * border_spacing_h;
|
|
if (relative_sum != 0) {
|
|
if (spare_width < 0)
|
|
spare_width = 0;
|
|
for (i = 0; i != columns; i++) {
|
|
if (col[i].type == COLUMN_WIDTH_RELATIVE) {
|
|
col[i].min = ceil(col[i].max =
|
|
(float) spare_width
|
|
* (float) col[i].width
|
|
/ relative_sum);
|
|
min_width += col[i].min;
|
|
max_width += col[i].max;
|
|
}
|
|
}
|
|
}
|
|
min_width += (columns + 1) * border_spacing_h;
|
|
max_width += (columns + 1) * border_spacing_h;
|
|
|
|
if (auto_width <= min_width) {
|
|
/* not enough space: minimise column widths */
|
|
for (i = 0; i < columns; i++) {
|
|
col[i].width = col[i].min;
|
|
}
|
|
table_width = min_width;
|
|
} else if (max_width <= auto_width) {
|
|
/* more space than maximum width */
|
|
if (table_width == AUTO) {
|
|
/* for auto-width tables, make columns max width */
|
|
for (i = 0; i < columns; i++) {
|
|
col[i].width = col[i].max;
|
|
}
|
|
table_width = max_width;
|
|
} else {
|
|
/* for fixed-width tables, distribute the extra space
|
|
* too */
|
|
unsigned int flexible_columns = 0;
|
|
for (i = 0; i != columns; i++)
|
|
if (col[i].type != COLUMN_WIDTH_FIXED)
|
|
flexible_columns++;
|
|
if (flexible_columns == 0) {
|
|
int extra = (table_width - max_width) / columns;
|
|
remainder = (table_width - max_width) -
|
|
(extra * columns);
|
|
for (i = 0; i != columns; i++) {
|
|
col[i].width = col[i].max + extra;
|
|
count -= remainder;
|
|
if (count < 0) {
|
|
col[i].width++;
|
|
count += columns;
|
|
}
|
|
}
|
|
|
|
} else {
|
|
int extra = (table_width - max_width) /
|
|
flexible_columns;
|
|
remainder = (table_width - max_width) -
|
|
(extra * flexible_columns);
|
|
for (i = 0; i != columns; i++)
|
|
if (col[i].type != COLUMN_WIDTH_FIXED) {
|
|
col[i].width = col[i].max +
|
|
extra;
|
|
count -= remainder;
|
|
if (count < 0) {
|
|
col[i].width++;
|
|
count += flexible_columns;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
/* space between min and max: fill it exactly */
|
|
float scale = (float) (auto_width - min_width) /
|
|
(float) (max_width - min_width);
|
|
/* fprintf(stderr, "filling, scale %f\n", scale); */
|
|
for (i = 0; i < columns; i++) {
|
|
col[i].width = col[i].min + (int) (0.5 +
|
|
(col[i].max - col[i].min) * scale);
|
|
}
|
|
table_width = auto_width;
|
|
}
|
|
|
|
xs[0] = x = border_spacing_h;
|
|
for (i = 0; i != columns; i++) {
|
|
if (!col[i].positioned)
|
|
x += col[i].width + border_spacing_h;
|
|
xs[i + 1] = x;
|
|
row_span[i] = 0;
|
|
excess_y[i] = 0;
|
|
row_span_cell[i] = 0;
|
|
}
|
|
|
|
/* position cells */
|
|
table_height = border_spacing_v;
|
|
for (row_group = table->children; row_group;
|
|
row_group = row_group->next) {
|
|
int row_group_height = 0;
|
|
for (row = row_group->children; row; row = row->next) {
|
|
int row_height = 0;
|
|
|
|
htype = css_computed_height(row->style, &value, &unit);
|
|
if (htype == CSS_HEIGHT_SET && unit != CSS_UNIT_PCT) {
|
|
row_height = FIXTOINT(css_unit_len2device_px(
|
|
row->style,
|
|
&content->unit_len_ctx,
|
|
value, unit));
|
|
}
|
|
for (c = row->children; c; c = c->next) {
|
|
assert(c->style);
|
|
c->width = xs[c->start_column + c->columns] -
|
|
xs[c->start_column] -
|
|
border_spacing_h -
|
|
c->border[LEFT].width -
|
|
c->padding[LEFT] -
|
|
c->padding[RIGHT] -
|
|
c->border[RIGHT].width;
|
|
c->float_children = 0;
|
|
c->cached_place_below_level = 0;
|
|
|
|
c->height = AUTO;
|
|
if (!layout_block_context(c, -1, content)) {
|
|
free(col);
|
|
free(excess_y);
|
|
free(row_span);
|
|
free(row_span_cell);
|
|
free(xs);
|
|
return false;
|
|
}
|
|
/* warning: c->descendant_y0 and
|
|
* c->descendant_y1 used as temporary storage
|
|
* until after vertical alignment is complete */
|
|
c->descendant_y0 = c->height;
|
|
c->descendant_y1 = c->padding[BOTTOM];
|
|
|
|
htype = css_computed_height(c->style,
|
|
&value, &unit);
|
|
|
|
if (htype == CSS_HEIGHT_SET &&
|
|
unit != CSS_UNIT_PCT) {
|
|
/* some sites use height="1" or similar
|
|
* to attempt to make cells as small as
|
|
* possible, so treat it as a minimum */
|
|
int h = FIXTOINT(css_unit_len2device_px(
|
|
c->style,
|
|
&content->unit_len_ctx,
|
|
value, unit));
|
|
if (c->height < h)
|
|
c->height = h;
|
|
}
|
|
/* specified row height is treated as a minimum
|
|
*/
|
|
if (c->height < row_height)
|
|
c->height = row_height;
|
|
c->x = xs[c->start_column] +
|
|
c->border[LEFT].width;
|
|
c->y = c->border[TOP].width;
|
|
for (i = 0; i != c->columns; i++) {
|
|
row_span[c->start_column + i] = c->rows;
|
|
excess_y[c->start_column + i] =
|
|
c->border[TOP].width +
|
|
c->padding[TOP] +
|
|
c->height +
|
|
c->padding[BOTTOM] +
|
|
c->border[BOTTOM].width;
|
|
row_span_cell[c->start_column + i] = 0;
|
|
}
|
|
row_span_cell[c->start_column] = c;
|
|
c->padding[BOTTOM] = -border_spacing_v -
|
|
c->border[TOP].width -
|
|
c->padding[TOP] -
|
|
c->height -
|
|
c->border[BOTTOM].width;
|
|
}
|
|
for (i = 0; i != columns; i++)
|
|
if (row_span[i] != 0)
|
|
row_span[i]--;
|
|
else
|
|
row_span_cell[i] = 0;
|
|
if (row->next || row_group->next) {
|
|
/* row height is greatest excess of a cell
|
|
* which ends in this row */
|
|
for (i = 0; i != columns; i++)
|
|
if (row_span[i] == 0 && row_height <
|
|
excess_y[i])
|
|
row_height = excess_y[i];
|
|
} else {
|
|
/* except in the last row */
|
|
for (i = 0; i != columns; i++)
|
|
if (row_height < excess_y[i])
|
|
row_height = excess_y[i];
|
|
}
|
|
for (i = 0; i != columns; i++) {
|
|
if (row_height < excess_y[i])
|
|
excess_y[i] -= row_height;
|
|
else
|
|
excess_y[i] = 0;
|
|
if (row_span_cell[i] != 0)
|
|
row_span_cell[i]->padding[BOTTOM] +=
|
|
row_height +
|
|
border_spacing_v;
|
|
}
|
|
|
|
row->x = 0;
|
|
row->y = row_group_height;
|
|
row->width = table_width;
|
|
row->height = row_height;
|
|
row_group_height += row_height + border_spacing_v;
|
|
}
|
|
row_group->x = 0;
|
|
row_group->y = table_height;
|
|
row_group->width = table_width;
|
|
row_group->height = row_group_height;
|
|
table_height += row_group_height;
|
|
}
|
|
/* Table height is either the height of the contents, or specified
|
|
* height if greater */
|
|
table_height = max(table_height, min_height);
|
|
/** \todo distribute spare height over the row groups / rows / cells */
|
|
|
|
/* perform vertical alignment */
|
|
for (row_group = table->children; row_group;
|
|
row_group = row_group->next) {
|
|
for (row = row_group->children; row; row = row->next) {
|
|
for (c = row->children; c; c = c->next) {
|
|
enum css_vertical_align_e vertical_align;
|
|
|
|
/* unextended bottom padding is in
|
|
* c->descendant_y1, and unextended
|
|
* cell height is in c->descendant_y0 */
|
|
spare_height = (c->padding[BOTTOM] -
|
|
c->descendant_y1) +
|
|
(c->height - c->descendant_y0);
|
|
|
|
vertical_align = css_computed_vertical_align(
|
|
c->style, &value, &unit);
|
|
|
|
switch (vertical_align) {
|
|
case CSS_VERTICAL_ALIGN_SUB:
|
|
case CSS_VERTICAL_ALIGN_SUPER:
|
|
case CSS_VERTICAL_ALIGN_TEXT_TOP:
|
|
case CSS_VERTICAL_ALIGN_TEXT_BOTTOM:
|
|
case CSS_VERTICAL_ALIGN_SET:
|
|
case CSS_VERTICAL_ALIGN_BASELINE:
|
|
/* todo: baseline alignment, for now
|
|
* just use ALIGN_TOP */
|
|
case CSS_VERTICAL_ALIGN_TOP:
|
|
break;
|
|
case CSS_VERTICAL_ALIGN_MIDDLE:
|
|
c->padding[TOP] += spare_height / 2;
|
|
c->padding[BOTTOM] -= spare_height / 2;
|
|
layout_move_children(c, 0,
|
|
spare_height / 2);
|
|
break;
|
|
case CSS_VERTICAL_ALIGN_BOTTOM:
|
|
c->padding[TOP] += spare_height;
|
|
c->padding[BOTTOM] -= spare_height;
|
|
layout_move_children(c, 0,
|
|
spare_height);
|
|
break;
|
|
case CSS_VERTICAL_ALIGN_INHERIT:
|
|
assert(0);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Top and bottom margins of 'auto' are set to 0. CSS2.1 10.6.3 */
|
|
if (table->margin[TOP] == AUTO)
|
|
table->margin[TOP] = 0;
|
|
if (table->margin[BOTTOM] == AUTO)
|
|
table->margin[BOTTOM] = 0;
|
|
|
|
free(col);
|
|
free(excess_y);
|
|
free(row_span);
|
|
free(row_span_cell);
|
|
free(xs);
|
|
|
|
table->width = table_width;
|
|
table->height = table_height;
|
|
|
|
return true;
|
|
}
|