diff --git a/src/vaev-layout/base.h b/src/vaev-layout/base.h index 79736b1..a2495de 100644 --- a/src/vaev-layout/base.h +++ b/src/vaev-layout/base.h @@ -31,7 +31,7 @@ enum struct Commit { struct Input { Commit commit = Commit::NO; //< Should the computed values be committed to the layout? - IntrinsicSize intrinsic = IntrinsicSize::AUTO; + Pair intrinsic = {IntrinsicSize::AUTO, IntrinsicSize::AUTO}; Math::Vec2> knownSize = {}; Vec2Px availableSpace = {}; Vec2Px containingBlock = {}; diff --git a/src/vaev-layout/flex.cpp b/src/vaev-layout/flex.cpp index 9a49cc7..47d558c 100644 --- a/src/vaev-layout/flex.cpp +++ b/src/vaev-layout/flex.cpp @@ -1,102 +1,484 @@ #include "flex.h" #include "frag.h" +#include "values.h" namespace Vaev::Layout { struct FlexItem { Frag &frag; + Flex const &flexContainer; + Output &output; + + Px flexBaseSize; + + Px usedMainSize, usedCrossSize; + InsetsPx margin{}; + + void commit() { + frag.layout.margin = margin; + } + + FlexItem(Frag &f, Flex const &flexContainer, Output &output) : frag(f), flexContainer(flexContainer), output(output) { + } + + inline bool isRowOriented() const { + return flexContainer.direction == FlexDirection::ROW or flexContainer.direction == FlexDirection::ROW_REVERSE; + } + + inline Px getScaledFlexShrinkFactor() const { + return flexBaseSize * Px{frag.style->flex->shrink}; + } + + void setOutput(Output &&out) { + output = out; + } + + void computeFlexBaseSize(Tree &t, Frag &f) { + if (frag.style->flex->basis.type == FlexBasis::WIDTH) { + flexBaseSize = resolve(t, f, frag.style->flex->basis.width.value, Px{0}); + } else { + // TODO: other flex base size cases + logWarn("not implemented flex base size case"); + } + } + + Px getHypotheticalMainSize() const { + logWarn("incorrect hypothetical main size since no clamping and flooring is being done"); + // TODO: correct computation of hypothetical main size + return flexBaseSize; + } }; struct FlexLine { - Slice items; + MutSlice items; + Px crossSize; + + FlexLine(MutSlice items) : items(items), crossSize(0) {} + + void alignFlexStart() { + Px currPositionX{0}; + for (auto &flexItem : items) { + flexItem.frag.layout.position = {currPositionX, Px{0}}; + currPositionX += flexItem.usedMainSize + flexItem.margin.horizontal(); + } + } + + void alignFlexEnd(Px mainSize, Px occupiedSize) { + // TODO: negative cases + Px currPositionX{mainSize - occupiedSize}; + for (auto &flexItem : items) { + flexItem.frag.layout.position = {currPositionX, Px{0}}; + currPositionX += flexItem.usedMainSize + flexItem.margin.horizontal(); + } + } + + void alignSpaceAround(Px mainSize, Px occupiedSize) { + Px gapSize = (mainSize - occupiedSize) / Px{items.len()}; + + Px currPositionX{gapSize / Px{2}}; + for (auto &flexItem : items) { + flexItem.frag.layout.position = {currPositionX, Px{0}}; + currPositionX += flexItem.usedMainSize + flexItem.margin.horizontal() + gapSize; + } + } + + void alignSpaceBetween(Px mainSize, Px occupiedSize) { + Px gapSize = (mainSize - occupiedSize) / Px{items.len() - 1}; + + Px currPositionX{0}; + for (auto &flexItem : items) { + flexItem.frag.layout.position = {currPositionX, Px{0}}; + currPositionX += flexItem.usedMainSize + flexItem.margin.horizontal() + gapSize; + } + } + + void alignCenter(Px mainSize, Px occupiedSize) { + // TODO: negative cases + Px currPositionX{(mainSize - occupiedSize) / Px{2}}; + for (auto &flexItem : items) { + flexItem.frag.layout.position = {currPositionX, Px{0}}; + currPositionX += flexItem.usedMainSize + flexItem.margin.horizontal(); + } + } + + void justifyContent(Style::Align::Keywords justifyContentParam, Px mainSize, Px occupiedSize) { + switch (justifyContentParam) { + case Style::Align::FLEX_START: + alignFlexStart(); + return; + + case Style::Align::SPACE_AROUND: + if (occupiedSize > mainSize or items.len() == 1) + alignCenter(mainSize, occupiedSize); + else + alignSpaceAround(mainSize, occupiedSize); + return; + + case Style::Align::CENTER: + alignCenter(mainSize, occupiedSize); + return; + + case Style::Align::FLEX_END: + alignFlexEnd(mainSize, occupiedSize); + return; + + case Style::Align::SPACE_BETWEEN: + alignSpaceBetween(mainSize, occupiedSize); + return; + + default: + // FIXME: what to do? + alignFlexStart(); + } + } }; +Px getAvailableSpace(Px availableSpaceFromInput, Opt knownSizeFromInput, IntrinsicSize intrinsicSize) { + Px availableSpace; + if (knownSizeFromInput) { + availableSpace = knownSizeFromInput.unwrap(); + } else if (intrinsicSize == IntrinsicSize::MIN_CONTENT) { + // TODO + } else if (intrinsicSize == IntrinsicSize::MAX_CONTENT) { + // TODO + } else { + // TODO: otherwise, subtract the flex container’s margin, border, and padding from the space available to the flex + // (knownSize does not account for these values, but what about input.availableSpace?) + // didnt find any reference to availableSpace in frag.cpp + availableSpace = availableSpaceFromInput; + } + return availableSpace; +} + +// input.knownSize are inner sizes Output flexLayout(Tree &t, Frag &f, Input input) { // https://www.w3.org/TR/css-flexbox-1/#layout-algorithm + Flex const &flexContainer = *f.style->flex; + bool isRowOriented = flexContainer.direction == FlexDirection::ROW or flexContainer.direction == FlexDirection::ROW_REVERSE; // 1. Generate anonymous flex items - Vec flexItems; - - // TODO: Implement this step + Vec flexItems{f.children().len()}; + for (auto &c : f.children()) { + auto output = layout( + t, + c, + Input{ + .commit = input.commit, + .knownSize = {NONE, NONE}, + // TODO: not really sure of these arguments + .availableSpace = {Px{0}, input.knownSize.y.unwrapOr(Px{0})}, + .containingBlock = {Px{0}, input.knownSize.y.unwrapOr(Px{0})}, + } + ); + flexItems.pushBack(FlexItem{c, flexContainer, output}); + } // 2. Determine the available main and cross space for the flex items. - // TODO: Implement this step + // TODO: Maybe refactor orientation after checking karm-math/Flow.h + Px availableMainSpace, availableCrossSpace; + if (flexContainer.direction == FlexDirection::ROW) { + availableMainSpace = getAvailableSpace(input.availableSpace.x, input.knownSize.x, input.intrinsic.car); + availableCrossSpace = getAvailableSpace(input.availableSpace.y, input.knownSize.y, input.intrinsic.cdr); + } else { + // TODO: implement other orientation cases + logWarn("column orientation or reverse-row still not implemented"); + availableMainSpace = getAvailableSpace(input.availableSpace.y, input.knownSize.y, input.intrinsic.cdr); + availableCrossSpace = getAvailableSpace(input.availableSpace.x, input.knownSize.x, input.intrinsic.car); + } // 3. Determine the flex base size and hypothetical main size of each item - // TODO: Implement this step + for (auto &flexItem : flexItems) + flexItem.computeFlexBaseSize(t, f); // 4. Determine the main size of the flex container - // TODO: Implement this step + // TODO: we should have the knownSize of the main size; not clear what to do in case it isnt there + auto mainSize = isRowOriented ? input.knownSize.x.unwrapOr(Px{0}) : input.knownSize.y.unwrapOr(Px{0}); // 5. Collect flex items into flex lines Vec flexLines; + if (flexContainer.wrap == FlexWrap::NOWRAP) { + flexLines = Vec{FlexLine{flexItems}}; + } else { + logWarn("multiple flex lines still not fully implemented"); + size_t startItemIdx = 0; + while (startItemIdx < flexItems.len()) { + size_t endItemIdx = startItemIdx; + Px currLineSize = Px{0}; + while (endItemIdx < flexItems.len()) { + // TODO: ignoring breaks for now + if (currLineSize + flexItems[endItemIdx].getHypotheticalMainSize() <= availableMainSpace) + currLineSize += flexItems[endItemIdx++].getHypotheticalMainSize(); + else + break; + } - // TODO: Implement this step + flexLines.pushBack(FlexLine{mutSub(flexItems, startItemIdx, endItemIdx)}); + + startItemIdx = endItemIdx; + } + } // 6. Resolve the flexible lengths - // TODO: Implement this step + for (auto &flexLine : flexLines) { + Px sumItemsHypotheticalMainSizes{0}; + for (auto const &flexItem : flexLine.items) { + sumItemsHypotheticalMainSizes += flexItem.getHypotheticalMainSize(); + } + + bool matchedSize = sumItemsHypotheticalMainSizes == mainSize; + bool flexCaseIsGrow = sumItemsHypotheticalMainSizes < mainSize; + + Vec unfrozenItems{flexLine.items.len()}, frozenItems{flexLine.items.len()}; + Px sumFrozenSizes{0}; + for (auto &flexItem : flexLine.items) { + if ( + matchedSize or + (flexCaseIsGrow and flexItem.flexBaseSize > flexItem.getHypotheticalMainSize()) or + (!flexCaseIsGrow and flexItem.flexBaseSize < flexItem.getHypotheticalMainSize()) or + (flexItem.frag.style->flex->grow == 0 and flexItem.frag.style->flex->shrink == 0) + ) { + flexItem.usedMainSize = flexItem.getHypotheticalMainSize() + flexItem.output.margins.horizontal(); + frozenItems.pushBack(&flexItem); + sumFrozenSizes += flexItem.usedMainSize; + } else { + flexItem.usedMainSize = flexItem.getHypotheticalMainSize() + flexItem.output.margins.horizontal(); + unfrozenItems.pushBack(&flexItem); + } + } + + auto computeStats = [&]() { + Px sumOfUnfrozenSizes{0}; + Number sumUnfrozenFlexFactors{0}; + for (auto *flexItem : unfrozenItems) { + sumOfUnfrozenSizes += flexItem->usedMainSize; + sumUnfrozenFlexFactors += flexCaseIsGrow ? flexItem->frag.style->flex->grow : flexItem->frag.style->flex->shrink; + } + + return Tuple(sumOfUnfrozenSizes, sumUnfrozenFlexFactors); + }; + + auto [sumUnfrozenSizes, _] = computeStats(); + // FIXME: weird types of spaces and sizes here, since free space can be negative + Number initialFreeSpace = Number{mainSize} - Number{sumUnfrozenSizes + sumFrozenSizes}; + + while (unfrozenItems.len()) { + auto [sumUnfrozenSizes, sumUnfrozenFlexFactors] = computeStats(); + auto freeSpace = Number{mainSize} - Number{sumUnfrozenSizes + sumFrozenSizes}; + + if (sumUnfrozenFlexFactors < 1 && abs(initialFreeSpace * sumUnfrozenFlexFactors) < abs(freeSpace)) + freeSpace = initialFreeSpace * sumUnfrozenFlexFactors; + + if (flexCaseIsGrow) { + for (auto *flexItem : unfrozenItems) { + Number ratio = flexItem->frag.style->flex->grow / sumUnfrozenFlexFactors; + flexItem->usedMainSize = flexItem->flexBaseSize + Px{ratio * freeSpace}; + } + } else { + Px sumScaledFlexShrinkFactor{0}; + for (auto *flexItem : unfrozenItems) { + sumScaledFlexShrinkFactor += flexItem->getScaledFlexShrinkFactor(); + } + for (auto *flexItem : frozenItems) { + sumScaledFlexShrinkFactor += flexItem->getScaledFlexShrinkFactor(); + } + for (auto *flexItem : unfrozenItems) { + Px ratio = flexItem->getScaledFlexShrinkFactor() / sumScaledFlexShrinkFactor; + flexItem->usedMainSize = flexItem->flexBaseSize - ratio * Px{freeSpace}; + } + } + + Px totalViolation{0}; + // assuming row here + // TODO: Clamp each non-frozen item’s target main size by its used min and max main sizes and floor its content-box size at zero. + logWarn("computation of flex item sizes should go under clamping, still not implemented"); + for (auto *flexItem : unfrozenItems) { + Px clampedSize = flexItem->usedMainSize; + totalViolation += clampedSize - flexItem->usedMainSize; + } + for (auto *flexItem : frozenItems) { + Px clampedSize = flexItem->usedMainSize; + totalViolation += clampedSize - flexItem->usedMainSize; + } + + if (totalViolation == Px{0}) { + for (auto *flexItem : unfrozenItems) + frozenItems.pushBack(flexItem); + unfrozenItems.clear(); + } else { + Vec indexesToFreeze; + if (totalViolation < Px{0}) { + for (usize i = 0; i < unfrozenItems.len(); ++i) { + auto *flexItem = unfrozenItems[i]; + Px clampedSize = flexItem->usedMainSize; + + if (clampedSize < flexItem->usedMainSize) + indexesToFreeze.pushBack(i); + } + } else { + for (usize i = 0; i < unfrozenItems.len(); ++i) { + auto *flexItem = unfrozenItems[i]; + Px clampedSize = flexItem->usedMainSize; + + if (clampedSize > flexItem->usedMainSize) + indexesToFreeze.pushBack(i); + } + } + + // TODO: reverse indexesToFreeze so we use foreach + for (int j = indexesToFreeze.len() - 1; j >= 0; j--) { + usize i = indexesToFreeze[j]; + + frozenItems.pushBack(unfrozenItems[i]); + std::swap(unfrozenItems[i], unfrozenItems[unfrozenItems.len() - 1]); + unfrozenItems.popBack(); + sumFrozenSizes += unfrozenItems[i]->usedMainSize; + } + } + } - // 7. Determine the hypothetical cross size of each item - // TODO: Implement this step + for (size_t i = 0; i < flexLine.items.len(); ++i) { + auto &flexItem = flexLine.items[i]; + // 7. Determine the hypothetical cross size of each item + + // TODO: once again, this was coded assuming a ROW orientation + Input itemInput{ + .commit = input.commit, + .knownSize = {flexItem.usedMainSize, NONE}, + .availableSpace = {flexItem.usedMainSize, input.knownSize.y.unwrapOr(Px{0})}, + .containingBlock = {flexItem.usedMainSize, input.knownSize.y.unwrapOr(Px{0})}, + }; + + if (flexItem.frag.style->sizing->width == Size::AUTO) + input.intrinsic.car = IntrinsicSize::STRETCH_TO_FIT; + + flexItem.setOutput(layout( + t, + flexItem.frag, + itemInput + )); + } + } // 8. Calculate the cross size of each flex line - // TODO: Implement this step + if (flexLines.len() == 1) { + flexLines[0].crossSize = input.knownSize.y.unwrapOr(Px{0}); + } else { + logWarn("still cannot compute cross size of flex lines when we have multiple of them"); + } // 9. Handle 'align-content: stretch'. - // TODO: Implement this step + if (f.style->aligns.alignContent == Style::Align::STRETCH) + logWarn("still not handling 'align-content: stretch' since we only have one flex line"); // 10. Collapse visibility:collapse items. - // TODO: Implement this step + // TODO: simplify first try (assume not the case) // 11. Determine the used cross size of each flex item. - // TODO: Implement this step + for (auto &flexLine : flexLines) { + for (auto &flexItem : flexLine.items) { + /* + If a flex item has align-self: stretch, its computed cross size property is auto, and neither of its + cross-axis margins are auto, the used outer cross size is the used cross size of its flex line, + clamped according to the item’s used min and max cross sizes. + the used outer cross size is the used cross size of its flex line, clamped according to the item’s used min + and max cross sizes. + */ + if ( + flexItem.frag.style->aligns.alignSelf == Style::Align::STRETCH and + flexItem.frag.style->sizing->height.type == Size::AUTO and + (flexItem.frag.style->margin->bottom == Width::AUTO or + flexItem.frag.style->margin->top == Width::AUTO + ) + ) { + // TODO: should be the OUTER cross size and should be clamped + flexItem.usedCrossSize = flexLine.crossSize; + } else { + // Otherwise, the used cross size is the item’s hypothetical cross size. + flexItem.usedCrossSize = flexItem.output.size.y; + } + } + } // 12. Distribute any remaining free space - // TODO: Implement this step + for (auto &flexLine : flexLines) { + Px occupiedMainSize{0}; + for (auto &flexItem : flexLine.items) { + occupiedMainSize += flexItem.usedMainSize + flexItem.margin.horizontal(); + } + + bool usedAutoMargins = false; + if (mainSize > occupiedMainSize) { + usize countOfAutos = 0; + for (auto &flexItem : flexLine.items) { + countOfAutos += (flexItem.frag.style->margin->start == Width::AUTO); + countOfAutos += (flexItem.frag.style->margin->end == Width::AUTO); + } + if (countOfAutos) { + Px marginsSize = (mainSize - occupiedMainSize) / Px{countOfAutos}; + for (auto &flexItem : flexLine.items) { + if (flexItem.frag.style->margin->start == Width::AUTO) + flexItem.margin.start = marginsSize; + if (flexItem.frag.style->margin->end == Width::AUTO) + flexItem.margin.end = marginsSize; + } + + usedAutoMargins = true; + occupiedMainSize = mainSize; + } + } + + if (not usedAutoMargins) { + // for (auto &flexItem : flexLine.items) { + // TODO: Otherwise, set all auto margins to zero. + // TODO: Where should I set this? + // } + } + + if (input.commit == Commit::YES) { + // This is done after any flexible lengths and any auto margins have been resolved. + // NOTE: if auto margins eat all free space, what will be justified? Seems useless to run in such case + // NOTE: justifying doesnt change sizes or margins, so it is only being run when committing + flexLine.justifyContent(f.style->aligns.justifyContent.keyword, mainSize, occupiedMainSize); + } + } // 13. Resolve cross-axis auto margins. - // TODO: Implement this step + for (auto &flexLine : flexLines) { + for (auto &flexItem : flexLine.items) { + if (flexItem.frag.style->margin->bottom == Width::AUTO or flexItem.frag.style->margin->top == Width::AUTO) { + if (flexItem.usedCrossSize < flexLine.crossSize) { + Px freeCrossSpace = flexLine.crossSize - flexItem.usedCrossSize; + flexItem.margin.bottom = flexItem.margin.top = freeCrossSpace / Px{2}; + } else { + // TODO + logWarn("not implemented case of cross size margin when there is no space left"); + } + } + } + } // 14. Align all flex items along the cross-axis. - // TODO: Implement this step + logWarn("still not aligning in the cross axis"); // 15. Determine the flex container's used cross size - // TODO: Implement this step + Px usedCrossSize{0}; + if (input.knownSize.y) + usedCrossSize = input.knownSize.y.unwrap(); + else + for (auto &flexLine : flexLines) + usedCrossSize += flexLine.crossSize; + // TODO: clamp usedCrossSize // 16. Align all flex lines - // TODO: Implement this step - - // HACK: Bellow rough approximation to get something working - - Px mainSize = Px{0}, crossSize = Px{0}; - Px knownCrossSize = input.knownSize.y.unwrapOr(Px{0}); - - for (auto &c : f.children()) { - Opt childKnowCrossSize = NONE; - if (c.style->sizing->width == Size::AUTO) - childKnowCrossSize = knownCrossSize; - - auto ouput = layout( - t, - c, - Input{ - .commit = input.commit, - .knownSize = {NONE, childKnowCrossSize}, - .availableSpace = {Px{0}, knownCrossSize}, - .containingBlock = {Px{0}, knownCrossSize}, - } - ); - - mainSize += ouput.size.x; - - if (input.commit == Commit::YES) - c.layout.position = {mainSize, Px{0}}; + // TODO: implement me - crossSize = max(crossSize, ouput.size.y); + if (input.commit == Commit::YES) { + for (auto flexItem : flexItems) + flexItem.commit(); } - return Output::fromSize({mainSize, crossSize}); + return Output::fromSize({mainSize, usedCrossSize}); } } // namespace Vaev::Layout diff --git a/src/vaev-layout/frag.cpp b/src/vaev-layout/frag.cpp index 3d6d721..5b3f8b0 100644 --- a/src/vaev-layout/frag.cpp +++ b/src/vaev-layout/frag.cpp @@ -245,7 +245,7 @@ Output layout(Tree &t, Frag &f, Input input) { // FIXME: Take box-sizing into account return s - padding.horizontal() - borders.horizontal(); }); - input.intrinsic = widthIntrinsicSize; + input.intrinsic.car = widthIntrinsicSize; auto [specifiedHeight, heightIntrinsicSize] = _computeSpecifiedSize(t, f, input, sizing->height); if (input.knownSize.height == NONE) { @@ -256,7 +256,7 @@ Output layout(Tree &t, Frag &f, Input input) { // FIXME: Take box-sizing into account return s - padding.vertical() - borders.vertical(); }); - input.intrinsic = heightIntrinsicSize; + input.intrinsic.cdr = heightIntrinsicSize; auto [size, _] = _contentLayout(t, f, input);