From 5d4d7070296e8f096bce996211c31d6901fe8fb6 Mon Sep 17 00:00:00 2001 From: Paulo Medeiros Date: Thu, 19 Sep 2024 16:50:39 +0200 Subject: [PATCH] wipping! --- src/vaev-layout/base.h | 2 +- src/vaev-layout/flex.cpp | 484 +++++++++++++++++++++++++++++++++------ src/vaev-layout/frag.cpp | 5 +- 3 files changed, 419 insertions(+), 72 deletions(-) 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 41951a5..a03dfee 100644 --- a/src/vaev-layout/flex.cpp +++ b/src/vaev-layout/flex.cpp @@ -1,138 +1,484 @@ #include "flex.h" #include "frag.h" +#include "values.h" + +/* +CONSTRAINTS: ignore MIN,MAX CONTENT +CONTAINER SIZE IS DEFINED +NOT USING AUTO + +*/ namespace Vaev::Layout { struct FlexItem { Frag &frag; - Flex &flexContainer; + Flex const &flexContainer; + Output &output; + + Px flexBaseSize; + + Px usedMainSize, usedCrossSize; + InsetsPx margin{}; + + void commit() { + frag.layout.margin = margin; + logDebug("IM HERE YOOOOOOOOOOOOOOOOO {}", margin); + } + + FlexItem(Frag &f, Flex const &flexContainer, Output &output) : frag(f), flexContainer(flexContainer), output(output) { + } - FlexItem(Frag &f, Flex &flexContainer) : frag(f), flexContainer(flexContainer) { + inline bool isRowOriented() const { + return flexContainer.direction == FlexDirection::ROW or flexContainer.direction == FlexDirection::ROW_REVERSE; } - Number getCrossSize() { - if (flexContainer.direction == FlexDirection::ROW or flexContainer.direction == FlexDirection::ROW_REVERSE) { + inline Px getScaledFlexShrinkFactor() const { + return flexBaseSize * Px{frag.style->flex->shrink}; + } + + // TODO: consider memoizing the following properties + Px getCrossSize() const { + if (isRowOriented()) { // return frag ... height } else { // return frag ... width } - return 0; + return Px{0}; + } + + void setOutput(Output &&out) { + output = out; } - Number getFlexBaseSize() { + void computeFlexBaseSize(Tree &t, Frag &f) { if (frag.style->flex->basis.type == FlexBasis::WIDTH) { - if (frag.style->flex->basis.width == Width::VALUE) - return frag.style->flex->basis.width.value; - } else { - if (getCrossSize() and - // frag.style. .... aspect ratio - true) { - // flex base size is calculated from its inner cross size and the flex item’s intrinsic aspect ratio. - } else { - // ... other conditions which are complex + flexBaseSize = resolve(t, f, frag.style->flex->basis.width.value, Px{0}); + } else if (frag.style->sizing->height.type == Size::Type::LENGTH /* and aspect ratio*/) { + auto mainCrossSize = resolve(t, f, frag.style->sizing->height.value, Px{0}); + + // flexBaseSize = mainCrossSize - f.style->padding->vertical(); + // if we have an aspect ratio it should be in the computed style + if (mainCrossSize /* and frag.style. .... aspect ratio*/) { + flexBaseSize = mainCrossSize; + } else if (false) { } } } + + Px getHypotheticalMainSize() const { + // TODO, (and flooring the content box size at zero). (max 0) + // min and max width only after calling layout + + auto flexHypSize = flexBaseSize; + // if (isRowOriented()) { + // flexHypSize = clamp(flexHypSize, frag.style.minWidth, frag.style.maxWidth); + // } else { + // flexHypSize = clamp(flexHypSize, frag.style.minHeight, frag.style.maxHeight); + // } + + return flexHypSize; + } }; 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: check if we have an infinite value for this number type + } else if (intrinsicSize == IntrinsicSize::MAX_CONTENT) { + } else { + // available space has margin? + // TODO^2: add a TODO in layout.cpp (didnt find where) + 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 - // TODO: Implement this step - Vec flexItems; + if (not(input.knownSize.x and input.knownSize.y) or not isRowOriented) + panic("hurting easy constraint"); + + // 1. Generate anonymous (all) flex items + Vec flexItems{f.children().len()}; for (auto &c : f.children()) { - // How does this behave for anon children? Dont know how children() returns them yet - flexItems.pushBack(FlexItem(c, f.style->flex.cow())); + auto output = layout( + t, + c, + Input{ + .commit = input.commit, + .knownSize = {NONE, NONE}, + // 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}); } + logDebug("{} {} {}", input.knownSize.x, input.knownSize.y, flexContainer.direction); // 2. Determine the available main and cross space for the flex items. - // CURRENT ASSUMPTIONS: - // - flow-direction: row - // - dimensions of containers are definite size - auto availableMainSpace = input.availableSpace.x; - auto availableCrossSpace = input.availableSpace.y; + // Check karm-math/Flow.h + + Px availableMainSpace, availableCrossSpace; + if (isRowOriented) { + availableMainSpace = getAvailableSpace(input.availableSpace.x, input.knownSize.x, input.intrinsic.car); + availableCrossSpace = getAvailableSpace(input.availableSpace.y, input.knownSize.y, input.intrinsic.cdr); + } else { + 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); + logDebug("flex base size{}", flexItem.flexBaseSize); + } // 4. Determine the main size of the flex container - // TODO: Implement this step + // Determine the main size of the flex container using the rules of the formatting context in which it participates + // NOTE: (ideally) this was already invoked before this method + auto mainSize = isRowOriented ? input.knownSize.x.unwrap() : input.knownSize.y.unwrap(); // 5. Collect flex items into flex lines - Vec - flexLines; + Vec flexLines; + if (flexContainer.wrap == FlexWrap::NOWRAP) { + flexLines = Vec{FlexLine{flexItems}}; + } else { + 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 + // availableMainSpace: assuming it is the containers inner size + 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(); + } - // 7. Determine the hypothetical cross size of each item - // TODO: Implement this step + // not sure haha + // GET This value from layout() + // NOTE: should we be worried about epsilon? + bool matchedSize = sumItemsHypotheticalMainSizes == mainSize; + + // TODO: "If the sum is less than the flex container’s inner main size" + bool flexCaseIsGrow = sumItemsHypotheticalMainSizes < mainSize; + + // separate frozen and unfrozen into different vecs? + Vec> targetSizes(flexLine.items.len()); + for (usize i = 0; i < flexLine.items.len(); ++i) { + auto &flexItem = flexLine.items[i]; + 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) + ) { + // initially frozen item + targetSizes.pushBack({true, flexItem.getHypotheticalMainSize() + flexItem.output.margins.horizontal()}); + } else { + targetSizes.pushBack({false, flexLine.items[i].flexBaseSize + flexItem.output.margins.horizontal()}); + } + } + + // FIXME: types of spaces and sizes here, since free space can be negative + auto computeStats = [&]() { + Px sumOfSizes{0}; + size_t countUnfrozen = 0; + Number sumUnfrozenFlexFactors{0}; + for (size_t i = 0; i < flexLine.items.len(); ++i) { + auto &flexItem = flexLine.items[i]; + sumOfSizes += targetSizes[i].v1; + if (not targetSizes[i].v0) { + countUnfrozen++; + sumUnfrozenFlexFactors += flexCaseIsGrow ? flexItem.frag.style->flex->grow : flexItem.frag.style->flex->shrink; + } + } + + return Tuple(sumOfSizes, countUnfrozen, sumUnfrozenFlexFactors); + }; + + auto [occupiedSize, countUnfrozen, sumUnfrozenFlexFactors] = computeStats(); + // Sum the outer sizes of all items on the line, and subtract this from the flex container’s inner main size. + // container’s inner main size: what should i use for this? input? frag? i suppose that input but im not sure + Number initialFreeSpace = Number{mainSize} - Number{occupiedSize}; + + while (countUnfrozen > 0) { + // call again before check or only after? + auto stats = computeStats(); + auto freeSpace = Number{mainSize} - Number{stats.v0}; + countUnfrozen = stats.v1; + sumUnfrozenFlexFactors = stats.v2; + + if (sumUnfrozenFlexFactors < 1 && abs(initialFreeSpace * sumUnfrozenFlexFactors) < abs(freeSpace)) + freeSpace = initialFreeSpace * sumUnfrozenFlexFactors; + + if (flexCaseIsGrow) { + // doesnt specify 'frozen', but seems to be the case + for (size_t i = 0; i < flexLine.items.len(); ++i) + if (not targetSizes[i].v0) { + auto &flexItem = flexLine.items[i]; + Number ratio = flexItem.frag.style->flex->grow / sumUnfrozenFlexFactors; + targetSizes[i].v1 = flexItem.flexBaseSize + Px{ratio * freeSpace}; + } + } else { + Px sumScaledFlexShrinkFactor{0}; + for (size_t i = 0; i < flexLine.items.len(); ++i) { + auto &flexItem = flexLine.items[i]; + sumScaledFlexShrinkFactor += flexItem.getScaledFlexShrinkFactor(); + } + for (size_t i = 0; i < flexLine.items.len(); ++i) + if (not targetSizes[i].v0) { + auto &flexItem = flexLine.items[i]; + Px ratio = flexItem.getScaledFlexShrinkFactor() / sumScaledFlexShrinkFactor; + targetSizes[i].v1 = flexItem.flexBaseSize - ratio * Px{freeSpace}; + } + } + + Px totalViolation{0}; + for (size_t i = 0; i < flexLine.items.len(); ++i) { + // auto &flexItem = flexLine.items[i]; + // 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. + Px clampedSize = targetSizes[i].v1; + totalViolation += clampedSize - targetSizes[i].v1; + } + + if (totalViolation == Px{0}) { + for (auto &targetSize : targetSizes) + targetSize.v0 = true; + } else if (totalViolation < Px{0}) { + for (size_t i = 0; i < flexLine.items.len(); ++i) { + // auto &flexItem = flexLine.items[i]; + // 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. + Px clampedSize = targetSizes[i].v1; + if (clampedSize < targetSizes[i].v1) + targetSizes[i].v0 = true; + } + } else { + for (size_t i = 0; i < flexLine.items.len(); ++i) { + // auto &flexItem = flexLine.items[i]; + // 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. + Px clampedSize = targetSizes[i].v1; + if (clampedSize > targetSizes[i].v1) + targetSizes[i].v0 = true; + } + } + } + + for (size_t i = 0; i < flexLine.items.len(); ++i) { + auto &flexItem = flexLine.items[i]; + // 7. Determine the hypothetical cross size of each item + // Set each item’s used main size to its target main size. (ROW) + + flexItem.usedMainSize = targetSizes[i].v1; + + 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}); + logDebug("{}", flexLines[0].crossSize); + } else { + // TODO + } // 9. Handle 'align-content: stretch'. - // TODO: Implement this step + // TODO: Note, this property has no effect on a single-line flex container. (current assumption) // 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 (false) { // TODO + + } 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(); + } + + 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; + } + + occupiedMainSize = mainSize; + } + } + 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?? + flexLine.justifyContent(f.style->aligns.justifyContent.keyword, mainSize, occupiedMainSize); + for (auto &flexItem : flexLine.items) + flexItem.commit(); + } + } // 13. Resolve cross-axis auto margins. // TODO: Implement this step // 14. Align all flex items along the cross-axis. - // TODO: Implement this step // 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; + // usedCrossSize: clamp // 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}}; - - crossSize = max(crossSize, ouput.size.y); - } - - 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..b697365 100644 --- a/src/vaev-layout/frag.cpp +++ b/src/vaev-layout/frag.cpp @@ -145,6 +145,7 @@ Output _contentLayout(Tree &t, Frag &f, Input input) { } else if (display == Display::FLOW or display == Display::FLOW_ROOT) { return blockLayout(t, f, input); } else if (display == Display::FLEX) { + logDebug("HEY OH LETS GO"); return flexLayout(t, f, input); } else if (display == Display::GRID) { return gridLayout(t, f, input); @@ -245,7 +246,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 +257,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);