Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,51 @@ namespace docraft::loom::pipeline {
const std::vector<float>& weights,
int count,
const std::vector<float>& floors = {});

/**
* @brief Inputs for resolve_fixed_and_flexible_amounts(). Grouped into a struct
* instead of positional parameters because weights/explicit_amounts/floors are
* three same-typed `vector<float>` in a row -- an easy place to pass the wrong one
* to the wrong slot without the compiler catching it.
*/
struct WeightedSplitRequest
{
/// Total amount (width or height) to divide among `count` items.
float available_amount = 0.0F;
/// Number of items to resolve a share for.
int count = 0;
/// Per-item weight, applied only to flexible items; a missing or non-positive
/// entry defaults to 1.0.
std::vector<float> weights;
/// Per-item explicit override; a missing or non-positive entry means that item
/// is flexible. Empty (the default) means every item is flexible.
std::vector<float> explicit_amounts;
/// Optional per-flexible-item floor (e.g. each item's own natural/measured
/// size on that axis); if non-empty (same size as `count`), no flexible item's
/// result is smaller than its floor.
std::vector<float> floors;
};

/**
* @brief Like distribute_weighted_amounts, but items can opt out of the weighted
* split entirely: an item with a positive `explicit_amounts[i]` is "fixed" -- it
* keeps that exact value, doesn't count toward the weight pool, and its amount is
* subtracted from `available_amount` first. Only what's left over is then split by
* weight (floored per `floors`) among the remaining, flexible items -- via a plain
* distribute_weighted_amounts() call restricted to that flexible subset, so both
* functions apply the exact same weight-default and floor rules.
*
* Both table-column-width call sites need this same two-tier split: a column with
* an explicit Cell width="..." on any row must resolve to exactly that width, and
* only the other columns compete for whatever space is left.
* DocraftLoomLayoutProcessor::resolve_table_column_widths calls this once each
* cell's natural width is known (passed as `floors`); DocraftLoomMeasureProcessor
* calls it earlier, to estimate a wrap budget before any cell has been measured
* (so with no `floors`). Sharing this function is what keeps the two in sync --
* without it, Measure's estimate can silently drift from what Layout later paints.
* @param request See WeightedSplitRequest.
* @return `request.count` resolved shares, summing to `request.available_amount`
* unless the floors force a flexible item above its weighted share.
*/
std::vector<float> resolve_fixed_and_flexible_amounts(const WeightedSplitRequest& request);
} // namespace docraft::loom::pipeline
75 changes: 14 additions & 61 deletions docraft/src/docraft/loom/pipeline/docraft_loom_layout_processor.cc
Original file line number Diff line number Diff line change
Expand Up @@ -627,67 +627,20 @@ namespace docraft::loom::pipeline {
(2.0F * table.padding())
: sum_natural;

// A column is "fixed" if the author gave it an explicit width, "flexible"
// otherwise. Both helpers just read geometry/table -- no bookkeeping needed.
const auto& weights = table.column_weights();
auto is_fixed = [&](int c) { return geometry.explicit_widths[static_cast<std::size_t>(c)] > 0.0F; };
auto column_weight = [&](int c) {
if (c >= 0 && c < static_cast<int>(weights.size()) && weights[static_cast<std::size_t>(c)] > 0.0F)
return weights[static_cast<std::size_t>(c)];
return 1.0F; // missing/non-positive weight defaults to 1.0, same as distribute_weighted_amounts()
};

std::vector resolved(static_cast<std::size_t>(cols), 0.0F);

// 1) Fixed columns keep their own width verbatim. `remaining` is what's
// actually left over for the flexible ones -- not the full available_width.
float remaining = available_width;
for (int c = 0; c < cols; ++c)
{
if (is_fixed(c))
{
resolved[static_cast<std::size_t>(c)] = geometry.explicit_widths[static_cast<std::size_t>(c)];
remaining -= resolved[static_cast<std::size_t>(c)];
}
}
remaining = std::max(0.0F, remaining);

// 2) Split `remaining` among the flexible columns by weight. total_weight
// only sums flexible columns' weights, so a fixed column's weight can't
// dilute anyone else's share -- this is the actual fix (see example above).
// Each share is floored at the column's own natural width: never squeeze a
// column narrower than its content.
float total_weight = 0.0F;
for (int c = 0; c < cols; ++c)
if (!is_fixed(c))
total_weight += column_weight(c);

float flexible_total = 0.0F;
for (int c = 0; c < cols; ++c)
{
if (is_fixed(c))
continue;
const float share = total_weight > 0.0F ? remaining * column_weight(c) / total_weight : 0.0F;
resolved[static_cast<std::size_t>(c)] =
std::max(geometry.natural_widths[static_cast<std::size_t>(c)], share);
flexible_total += resolved[static_cast<std::size_t>(c)];
}

// 3) The floor in step 2 can push a column above its weighted share, so the
// flexible columns might no longer add up to `remaining`. Scale just those
// columns (fixed ones stay untouched) so the total matches available_width
// whenever the floors allow it. If the floors alone already exceed
// `remaining`, this scales below 1 and shrinks columns under their natural
// width -- content genuinely doesn't fit, but the table still stays close to
// available_width instead of overflowing it freely.
if (flexible_total > 0.0F && remaining > 0.0F)
{
const float scale = remaining / flexible_total;
for (int c = 0; c < cols; ++c)
if (!is_fixed(c))
resolved[static_cast<std::size_t>(c)] *= scale;
}
return resolved;
// Fixed columns (explicit_widths[c] > 0) keep their own width verbatim and
// reserve it out of available_width before flexible columns split what's left
// by weight, floored at each flexible column's own natural width so content
// never gets squeezed narrower than it needs -- see
// resolve_fixed_and_flexible_amounts for the shared fixed/flexible algorithm
// (also used by DocraftLoomMeasureProcessor's table wrap-budget estimate, so
// both agree on which columns are fixed).
return resolve_fixed_and_flexible_amounts({
.available_amount = available_width,
.count = cols,
.weights = table.column_weights(),
.explicit_amounts = geometry.explicit_widths,
.floors = geometry.natural_widths,
});
}

// Horizontal offset for content re-centered within a wider resolved column: text
Expand Down
74 changes: 49 additions & 25 deletions docraft/src/docraft/loom/pipeline/docraft_loom_measure_processor.cc
Original file line number Diff line number Diff line change
Expand Up @@ -494,38 +494,62 @@
const int rows = table->row_count();
const int cols = table->column_count();

// Best-effort per-column wrap ceiling, computed the same way Layout will later
// resolve column widths (explicit_width(), else a weight-based share, else an
// even split) -- see DocraftLoomLayoutProcessor::resolve_table_column_widths for
// the authoritative post-Measure version. This is only an upfront estimate so
// over-long cell text wraps instead of silently overflowing its column; it isn't
// pushed unconditionally (see visit(DocraftLoomTableCell*)), so it never disturbs
// the natural-width-floor sizing of cells that already fit.
std::vector<float> column_wrap_budget(static_cast<std::size_t>(cols), 0.0F);
if (cols > 0 && incoming_width > 0.0F) {
const float available_width =
incoming_width - (2.0F * nodes::DocraftLoomTable::kCellPaddingX) - (2.0F * table->padding());
if (available_width > 0.0F) {
const auto shares = distribute_weighted_amounts(available_width, table->column_weights(), cols);
for (int c = 0; c < cols; ++c) {
column_wrap_budget[static_cast<std::size_t>(c)] =
std::max(0.0F, shares[static_cast<std::size_t>(c)] -
(2.0F * nodes::DocraftLoomTable::kCellPaddingX));
// A column counts as "fixed" as soon as ONE of its cells sets Cell width="...",
// no matter which row -- so this scans every row before measuring any cell,
// not just the row currently being visited. Without this, a row that omits
// width() (inheriting the column from a sibling row) would look flexible here,
// and the wrap budget below would shrink that column instead of matching what
// it's actually painted at. Same idea as
// DocraftLoomLayoutProcessor::gather_table_natural_geometry's explicit_widths,
// minus natural_widths -- no cell has been measured yet at this point.
std::vector column_explicit_widths(static_cast<std::size_t>(cols), 0.0F);
for (int r = 0; r < rows; ++r) {
for (int c = 0; c < cols; ++c) {
if (auto width = table->cell(r, c)->explicit_width()) {

Check failure on line 508 in docraft/src/docraft/loom/pipeline/docraft_loom_measure_processor.cc

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use has_value() or another member function to clarify that the code tests the presence of a value in the "optional", not the contained "float" value itself.

See more on https://sonarcloud.io/project/issues?id=Cadons_Docraft&issues=AaALm27x41jXsmVCwhW2&open=AaALm27x41jXsmVCwhW2&pullRequest=84
column_explicit_widths[static_cast<std::size_t>(c)] =
std::max(column_explicit_widths[static_cast<std::size_t>(c)], *width);
}
}
}

std::vector<float> col_widths(static_cast<std::size_t>(cols), 0.0F);
std::vector<float> row_heights(static_cast<std::size_t>(rows), 0.0F);
// Upfront estimate of each column's wrap ceiling, using the same fixed-vs-
// flexible split Layout performs for real once every cell is measured (see
// resolve_table_column_widths) -- fixed columns keep column_explicit_widths
// verbatim, flexible columns split whatever's left by weight. The one thing
// this pass can't do yet is floor flexible columns at their natural width
// (nothing has been measured), so it's an estimate, not the final word: a cell
// only wraps if its own text turns out wider than this budget (see
// visit(DocraftLoomTableCell*)), so a short cell in a wide column is never
// disturbed by an under-estimate here.
//
// resolve_fixed_and_flexible_amounts() always honors a fixed column's own
// explicit width, even when available_width below is 0 -- so a cell with its
// own width() still gets a real wrap budget with no page/content width set at
// all; only the flexible columns then get no budget (0), same as before.
float available_width = 0.0F;
if (incoming_width > 0.0F) {
available_width = std::max(
0.0F, incoming_width - (2.0F * nodes::DocraftLoomTable::kCellPaddingX) - (2.0F * table->padding()));
}
const auto resolved_widths = resolve_fixed_and_flexible_amounts({
.available_amount = available_width,
.count = cols,
.weights = table->column_weights(),
.explicit_amounts = column_explicit_widths,
});
std::vector column_wrap_budget(static_cast<std::size_t>(cols), 0.0F);
for (int c = 0; c < cols; ++c) {
column_wrap_budget[static_cast<std::size_t>(c)] =
std::max(0.0F, resolved_widths[static_cast<std::size_t>(c)] -
(2.0F * nodes::DocraftLoomTable::kCellPaddingX));
}

std::vector col_widths(static_cast<std::size_t>(cols), 0.0F);
std::vector row_heights(static_cast<std::size_t>(rows), 0.0F);
for (int r = 0; r < rows; ++r) {
for (int c = 0; c < cols; ++c) {
auto cell = table->cell(r, c);
// An explicit per-cell width is a harder, more specific constraint than
// the column estimate above -- prefer it when set.
pending_cell_wrap_budget_ =
cell->explicit_width().has_value()
? std::max(0.0F, *cell->explicit_width() - (2.0F * nodes::DocraftLoomTable::kCellPaddingX))
: column_wrap_budget[static_cast<std::size_t>(c)];
pending_cell_wrap_budget_ = column_wrap_budget[static_cast<std::size_t>(c)];
cell->accept(*this);
// Cell's own measured_size already folds in its padding inset (see
// DocraftLoomTableCell's own Measure visit above) -- no extra term here.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,66 @@
}
return resolved;
}

std::vector<float> resolve_fixed_and_flexible_amounts(const WeightedSplitRequest& request)
{
const int count = request.count;
std::vector<float> resolved(static_cast<std::size_t>(std::max(count, 0)), 0.0F);

Check warning on line 56 in docraft/src/docraft/loom/pipeline/docraft_loom_weighted_distribution.cc

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Avoid explicitly specifying the template arguments by relying on the class template argument deduction.

See more on https://sonarcloud.io/project/issues?id=Cadons_Docraft&issues=AaALm22641jXsmVCwhW1&open=AaALm22641jXsmVCwhW1&pullRequest=84
if (count <= 0)
{
return resolved;
}

auto is_fixed = [&](int i) {
return i < static_cast<int>(request.explicit_amounts.size()) &&
request.explicit_amounts[static_cast<std::size_t>(i)] > 0.0F;
};

// 1) Fixed items keep their own amount verbatim, and their share is subtracted
// from `remaining` so it's never up for grabs by the flexible items below.
// While reserving fixed items, also collect the flexible ones' own weight/floor
// into their own compacted vectors (indices 0..flexible_count-1), so step 2 can
// hand them to a plain distribute_weighted_amounts() call.
float remaining = request.available_amount;
std::vector<int> flexible_indices;
std::vector<float> flexible_weights;
std::vector<float> flexible_floors;
const bool has_floors = !request.floors.empty();
for (int i = 0; i < count; ++i)
{
if (is_fixed(i))
{
resolved[static_cast<std::size_t>(i)] = request.explicit_amounts[static_cast<std::size_t>(i)];
remaining -= resolved[static_cast<std::size_t>(i)];
continue;
}
flexible_indices.push_back(i);
flexible_weights.push_back(
i < static_cast<int>(request.weights.size()) ? request.weights[static_cast<std::size_t>(i)] : 0.0F);
if (has_floors)
flexible_floors.push_back(request.floors[static_cast<std::size_t>(i)]);
}
remaining = std::max(0.0F, remaining);

// 2) Split `remaining` among just the flexible items -- reusing
// distribute_weighted_amounts() here (instead of re-deriving its weight-default
// and floor logic) is what guarantees a flexible item is treated identically
// whether it's resolved through this function or a plain weighted split.
const auto flexible_shares = distribute_weighted_amounts(
remaining, flexible_weights, static_cast<int>(flexible_indices.size()), flexible_floors);

float flexible_total = 0.0F;
for (float share : flexible_shares)
flexible_total += share;

// 3) A floor in step 2 can push a flexible item above its weighted share, so the
// flexible items might no longer add up to `remaining`. Scale just those items
// (fixed ones stay untouched) so the total matches `available_amount` whenever
// the floors allow it.
const float scale = (flexible_total > 0.0F && remaining > 0.0F) ? remaining / flexible_total : 1.0F;
for (std::size_t k = 0; k < flexible_indices.size(); ++k)
resolved[static_cast<std::size_t>(flexible_indices[k])] = flexible_shares[k] * scale;

return resolved;
}
} // namespace docraft::loom::pipeline
Loading
Loading