fix(runtime): keep pre-7.3 files on the legacy text sizing path (#13886) 9fe2b415a7 #13223 made a layout-controlled text's box always match its layout slot, and let overflow modes engage for auto-sized text inside layouts. The fix is right, but it changes how existing files render: wherever a hug layout's min/max made the slot disagree with the content, bounds move — and overflow properties that had always been inert suddenly take effect, so a text someone set to ellipsis years ago and saw do nothing now truncates. Gate the new behavior on the file's riv version. Text::import stamps m_layoutSizesBox from the import stack (>= 7.3); below that, bounds fall back to content size and overflowAsFixed() collapses to its pre-#13223 form. Every call site already routed through that one predicate, so the legacy path is an exact revert rather than an approximation. 7.3 is the existing bump from #13400 (Aug 5) rather than a new one, and that dates well: it postdates #13223 (Jul 24), so every file stamped 7.3 genuinely has the new behavior. Only exports from the Jul 24 – Aug 5 window are misclassified as legacy. Co-authored-by: Philip Chung <philterdesign@gmail.com>
diff --git a/.rive_head b/.rive_head index 9e5f012..358d82e 100644 --- a/.rive_head +++ b/.rive_head
@@ -1 +1 @@ -636e181568efe063143289c67fda6cab49143f0c +9fe2b415a70bb2d6460a9df2c2e4a6caf8589c30
diff --git a/include/rive/text/text.hpp b/include/rive/text/text.hpp index 874bfae..62b2d5e 100644 --- a/include/rive/text/text.hpp +++ b/include/rive/text/text.hpp
@@ -237,6 +237,8 @@ AABB constraintBounds() const override { return localBounds(); } void originXChanged() override; void originYChanged() override; + StatusCode import(ImportStack& importStack) override; + Core* clone() const override; Vec2D measureLayout(float width, LayoutMeasureMode widthMode, @@ -254,11 +256,24 @@ { return std::isnan(m_layoutHeight) ? height() : m_layoutHeight; } - // Overflow treats the box as fixed once a layout controls our size. + // Overflow treats the box as fixed once a layout sizes our box. bool overflowAsFixed() const { return effectiveSizing() == TextSizing::fixed || - !std::isnan(m_layoutWidth); + !std::isnan(layoutBoxWidth()); + } + // The size a controlling layout imposes on our *box*, or NAN when it + // imposes none. Not the same as effectiveWidth/Height above, which use + // m_layoutWidth/Height whenever a layout controls us: before 7.3 the + // layout sized the text but an auto-sized box still came from the + // content, so these are NAN there. See Text::import. + float layoutBoxWidth() const + { + return m_layoutSizesBox ? m_layoutWidth : NAN; + } + float layoutBoxHeight() const + { + return m_layoutSizesBox ? m_layoutHeight : NAN; } float computedWidth() override { return localBounds().width(); }; float computedHeight() override { return localBounds().height(); }; @@ -379,6 +394,9 @@ uint8_t m_layoutWidthScaleType = std::numeric_limits<uint8_t>::max(); uint8_t m_layoutHeightScaleType = std::numeric_limits<uint8_t>::max(); LayoutDirection m_layoutDirection = LayoutDirection::inherit; + // Whether a controlling layout sizes our box, not just our text. Stamped + // at import from the file version; true for anything built in memory. + bool m_layoutSizesBox = true; Vec2D measure(Vec2D maxSize); }; } // namespace rive
diff --git a/src/text/text.cpp b/src/text/text.cpp index 2d703a8..7ac912a 100644 --- a/src/text/text.cpp +++ b/src/text/text.cpp
@@ -1,5 +1,6 @@ #include "rive/text/text.hpp" #include "rive/layout/layout_participant.hpp" +#include "rive/importers/import_stack.hpp" using namespace rive; #ifdef WITH_RIVE_TEXT #include "rive/text_engine.hpp" @@ -602,19 +603,20 @@ // Step 4: update bounds. A layout-controlled axis uses the layout's size // so the text box always matches the layout. const float paragraphSpace = fitParagraphSpacing(); + const float boxWidth = layoutBoxWidth(); + const float boxHeight = layoutBoxHeight(); const float autoSizeMaxY = - std::isnan(m_layoutHeight) + std::isnan(boxHeight) ? std::max(minY, totalHeight - paragraphSpace - topTrim - bottomTrim) - : minY + m_layoutHeight; + : minY + boxHeight; switch (effectiveSizing()) { case TextSizing::autoWidth: - m_bounds = - AABB(0.0f, - minY, - std::isnan(m_layoutWidth) ? maxWidth : m_layoutWidth, - autoSizeMaxY); + m_bounds = AABB(0.0f, + minY, + std::isnan(boxWidth) ? maxWidth : boxWidth, + autoSizeMaxY); break; case TextSizing::autoHeight: m_bounds = AABB(0.0f, minY, effectiveWidth(), autoSizeMaxY); @@ -1533,6 +1535,26 @@ {} #endif +StatusCode Text::import(ImportStack& importStack) +{ + // A layout has always sized the text itself (effectiveWidth/Height), but + // before 7.3 an auto-sized text still took its *box* from the content, so + // the box could disagree with the slot and every overflow mode stayed + // inert. Keep that for those files; newer ones box to the slot. See + // File::minorVersion. + int major = importStack.majorVersion(); + int minor = importStack.minorVersion(); + m_layoutSizesBox = major > 7 || (major == 7 && minor >= 3); + return Super::import(importStack); +} + +Core* Text::clone() const +{ + Text* twin = TextBase::clone()->as<Text>(); + twin->m_layoutSizesBox = m_layoutSizesBox; + return twin; +} + Vec2D Text::layoutBaseTranslation(LayoutParticipant* participant) const { assert(participant != nullptr);
diff --git a/tests/unit_tests/assets/layout/text_layout_7_3.riv b/tests/unit_tests/assets/layout/text_layout_7_3.riv new file mode 100644 index 0000000..990bc5f --- /dev/null +++ b/tests/unit_tests/assets/layout/text_layout_7_3.riv Binary files differ
diff --git a/tests/unit_tests/assets/layout/text_layout_pre_7_3.riv b/tests/unit_tests/assets/layout/text_layout_pre_7_3.riv new file mode 100644 index 0000000..9e66720 --- /dev/null +++ b/tests/unit_tests/assets/layout/text_layout_pre_7_3.riv Binary files differ
diff --git a/tests/unit_tests/assets/layout_text_match.riv b/tests/unit_tests/assets/layout_text_match.riv index a5592b4..dea469f 100644 --- a/tests/unit_tests/assets/layout_text_match.riv +++ b/tests/unit_tests/assets/layout_text_match.riv Binary files differ
diff --git a/tests/unit_tests/assets/layout_text_match_7_3.riv b/tests/unit_tests/assets/layout_text_match_7_3.riv new file mode 100644 index 0000000..f8551a6 --- /dev/null +++ b/tests/unit_tests/assets/layout_text_match_7_3.riv Binary files differ
diff --git a/tests/unit_tests/runtime/text_test.cpp b/tests/unit_tests/runtime/text_test.cpp index 04c0cf5..2163923 100644 --- a/tests/unit_tests/runtime/text_test.cpp +++ b/tests/unit_tests/runtime/text_test.cpp
@@ -928,10 +928,11 @@ CHECK(silver.matches("text_vertical_trim_test")); } -TEST_CASE("Text box matches layout-controlled size", "[silver]") +// Renders a text/layout matrix for five frames against the named silver. +static void checkTextLayoutSilver(const char* asset, const char* silverName) { rive::SerializingFactory silver; - auto file = ReadRiveFile("assets/layout_text_match.riv", &silver); + auto file = ReadRiveFile(asset, &silver); auto artboard = file->artboardDefault(); REQUIRE(artboard != nullptr); @@ -973,7 +974,45 @@ artboard->draw(renderer.get()); } - CHECK(silver.matches("layout_text_match")); + CHECK(silver.matches(silverName)); +} + +// These two assets are the same scene and differ only in their header's minor +// version (see gen_layout_text_match.py), so any difference between the two +// silvers is attributable to the Text::import gate and nothing else. +// +// Below 7.3, m_layoutSizesBox stays false: an auto-sized text keeps +// content-sized bounds and every overflow mode stays inert, so the six +// overflow modes in each half of the matrix all render identically. +TEST_CASE("Text box keeps its content size before 7.3", "[silver]") +{ + checkTextLayoutSilver("assets/layout_text_match.riv", "layout_text_match"); +} + +// At 7.3 the box takes the layout's size and the overflow modes engage, so the +// matrix fans out. Rows 15-18 cover verticalAlign middle/bottom over both a +// taller box (minHeight) and a shorter one (maxHeight, where align and line +// culling interact). +TEST_CASE("Text box matches layout-controlled size", "[silver]") +{ + checkTextLayoutSilver("assets/layout_text_match_7_3.riv", + "layout_text_match_7_3"); +} + +// The two assets are the same scene apart from the version stamp and two +// inert ComponentOrigin children (the editor materialises those on selection; +// neither file carries a pivotOrigin), so they are authored exports rather +// than a generated pair. +TEST_CASE("Middle-aligned hug-layout text before 7.3", "[silver]") +{ + checkTextLayoutSilver("assets/layout/text_layout_pre_7_3.riv", + "text_layout_pre_7_3"); +} + +TEST_CASE("Middle-aligned hug-layout text at 7.3", "[silver]") +{ + checkTextLayoutSilver("assets/layout/text_layout_7_3.riv", + "text_layout_7_3"); } TEST_CASE("Fit font size with varying sizes", "[text]")
diff --git a/tests/unit_tests/silvers/data_viz_demo.sriv b/tests/unit_tests/silvers/data_viz_demo.sriv index a1914ca..3198c38 100644 --- a/tests/unit_tests/silvers/data_viz_demo.sriv +++ b/tests/unit_tests/silvers/data_viz_demo.sriv Binary files differ
diff --git a/tests/unit_tests/silvers/layout_text_match.sriv b/tests/unit_tests/silvers/layout_text_match.sriv index 6831c3b..c3e9c36 100644 --- a/tests/unit_tests/silvers/layout_text_match.sriv +++ b/tests/unit_tests/silvers/layout_text_match.sriv Binary files differ
diff --git a/tests/unit_tests/silvers/layout_text_match_7_3.sriv b/tests/unit_tests/silvers/layout_text_match_7_3.sriv new file mode 100644 index 0000000..9f346d0 --- /dev/null +++ b/tests/unit_tests/silvers/layout_text_match_7_3.sriv Binary files differ
diff --git a/tests/unit_tests/silvers/text_layout_7_3.sriv b/tests/unit_tests/silvers/text_layout_7_3.sriv new file mode 100644 index 0000000..691a9ae --- /dev/null +++ b/tests/unit_tests/silvers/text_layout_7_3.sriv Binary files differ
diff --git a/tests/unit_tests/silvers/text_layout_pre_7_3.sriv b/tests/unit_tests/silvers/text_layout_pre_7_3.sriv new file mode 100644 index 0000000..5b4aeed --- /dev/null +++ b/tests/unit_tests/silvers/text_layout_pre_7_3.sriv Binary files differ