88#include < layout/Positioner.hpp>
99
1010#include " ../tricks/Tricks.hpp"
11+ #include " element/Element.hpp"
12+ #include " hyprtoolkit/element/Element.hpp"
13+ #include " hyprtoolkit/element/Null.hpp"
14+ #include " hyprtoolkit/element/Rectangle.hpp"
15+ #include " hyprtoolkit/element/RowLayout.hpp"
16+ #include " hyprtoolkit/types/FontTypes.hpp"
17+ #include " hyprtoolkit/types/SizeType.hpp"
1118
1219using namespace Hyprtoolkit ;
1320using namespace Hyprutils ::Math;
@@ -25,60 +32,184 @@ TEST(Element, text) {
2532 text.reset ();
2633}
2734
28- // an auto label gets no room hint and its box tracks its own clamped preferred, so the box feeds
29- // back into the next layout. a transient narrow box on first layout (e.g. before an async icon
30- // sibling reflows the row) must not lock the text ellipsized: once the box equals the clamped
31- // preferred the clamp has to release and the text recover. #94 compared the box against a cached
32- // natural size, so the box-feedback condition stayed true and the ellipsis locked forever. this is
33- // the hyprlauncher regression.
34- TEST (Element, textEllipsizeRecoversFromTransientClamp) {
35+ TEST (Element, textPreferredSize) {
3536 Tests::Tricks::createBackendSupport ();
3637
37- auto text = CTextBuilder::begin ()->text (" Hello World Foo Bar Baz" )->commence ();
38+ auto text = CTextBuilder::begin ()->text (" Hello World Foo Bar Baz" )-> noEllipsize ( true )->commence ();
3839
39- // natural width: a generous box, no room hint
40- g_positioner->position (text, {{}, {1000 .F , 20 .F }}, {-1 .F , -1 .F });
41- const float NATURAL = text->preferredSize ({}).value_or (Vector2D{}).x ;
42- EXPECT_GT (NATURAL , 40 .F );
40+ const auto NATURAL_SIZE = text->preferredSize ({0 , 0 });
41+ EXPECT_TRUE (NATURAL_SIZE .has_value ());
4342
44- // a transient narrow box on first layout clamps it
45- g_positioner->position (text, {{}, {NATURAL * 0 .3F , 20 .F }}, {-1 .F , -1 .F });
46- EXPECT_LT (text->preferredSize ({}).value_or (Vector2D{}).x , NATURAL );
43+ const auto CLAMPED_WIDTH = std::floor (NATURAL_SIZE ->x * 3 / 4 );
4744
48- // from here the layout hands the auto label a box equal to its own preferred. feed that back a
49- // few frames: it must converge back to NATURAL, not stay locked at the clamped width.
50- for (int i = 0 ; i < 4 ; ++i) {
51- const auto PREF = text->preferredSize ({}).value_or (Vector2D{});
52- g_positioner->position (text, {{}, PREF }, {-1 .F , -1 .F });
53- }
54- EXPECT_FLOAT_EQ (text->preferredSize ({}).value_or (Vector2D{}).x , NATURAL );
45+ const auto WRAPPED_SIZE = text->preferredSize ({CLAMPED_WIDTH , NATURAL_SIZE ->y * 3 });
46+ EXPECT_TRUE (WRAPPED_SIZE .has_value ());
47+ EXPECT_LE (WRAPPED_SIZE ->x , CLAMPED_WIDTH );
48+ EXPECT_GT (WRAPPED_SIZE ->y , NATURAL_SIZE ->y );
49+
50+ text->rebuild ()->noEllipsize (false )->commence ();
51+ const auto ELLIPSIZED_SIZE = text->preferredSize ({CLAMPED_WIDTH , NATURAL_SIZE ->y * 3 });
52+ EXPECT_TRUE (ELLIPSIZED_SIZE .has_value ());
53+ EXPECT_LE (ELLIPSIZED_SIZE ->x , CLAMPED_WIDTH );
54+ EXPECT_EQ (ELLIPSIZED_SIZE ->y , NATURAL_SIZE ->y );
55+
56+ const auto FINAL_NATURAL_SIZE = text->preferredSize ({0 , 0 });
57+ EXPECT_TRUE (FINAL_NATURAL_SIZE .has_value ());
58+ EXPECT_EQ (FINAL_NATURAL_SIZE ->x , NATURAL_SIZE ->x );
59+ EXPECT_EQ (FINAL_NATURAL_SIZE ->y , NATURAL_SIZE ->y );
5560
5661 text.reset ();
5762}
5863
59- // a stable room hint (e.g. the combobox row layout) clamps to the room and stays put, no per-frame
60- // flicker at the fit/elide boundary.
61- TEST (Element, textEllipsizeStableUnderRoomHint) {
64+ TEST (Element, textGrowsAgainstNeighbor) {
6265 Tests::Tricks::createBackendSupport ();
6366
64- auto text = CTextBuilder::begin ()->text (" Hello World Foo Bar Baz" )->commence ();
65- const float NATURAL = text->preferredSize ({}).value_or (Vector2D{}).x ;
66- EXPECT_GT (NATURAL , 20 .F );
67+ const CBox POSITION = {0 , 0 , 1000 , 100 };
68+
69+ auto layout = CRowLayoutBuilder::begin ()->commence ();
70+ auto text = CTextBuilder::begin ()->text (" Hello World Foo Bar Baz" )->commence ();
71+ auto rect = CRectangleBuilder::begin ()->size ({CDynamicSize::HT_SIZE_AUTO , CDynamicSize::HT_SIZE_ABSOLUTE , {1 , 10 }})->commence ();
72+ rect->setGrow (true );
73+
74+ layout->addChild (text);
75+ layout->addChild (rect);
6776
68- const CBox widebox = {{}, {NATURAL , 20 .F }};
69- const Vector2D tight = {NATURAL * 0 .4F , 20 .F };
77+ g_positioner->position (layout, POSITION );
7078
71- g_positioner->position (text, widebox, tight);
72- const float CLAMPED = text->preferredSize ({}).value_or (Vector2D{}).x ;
73- EXPECT_LT (CLAMPED , NATURAL );
79+ EXPECT_EQ (rect->impl ->position .width + text->impl ->position .width , POSITION .width );
80+ const auto PREVIOUS_TEXT_WIDTH = text->impl ->position .width ;
7481
75- // same room again does not flip the size
76- g_positioner->position (text, widebox, tight);
77- EXPECT_FLOAT_EQ (text->preferredSize ({}).value_or (Vector2D{}).x , CLAMPED );
82+ text->rebuild ()->text (" Hello World Foo Bar Baz but longer" )->commence ();
83+ g_positioner->position (layout, POSITION );
7884
79- // room grows back: recover to full
80- g_positioner->position (text, widebox, {NATURAL + 80 .F , 20 .F });
81- EXPECT_FLOAT_EQ (text->preferredSize ({}).value_or (Vector2D{}).x , NATURAL );
85+ EXPECT_EQ (rect->impl ->position .width + text->impl ->position .width , POSITION .width );
86+ EXPECT_GT (text->impl ->position .width , PREVIOUS_TEXT_WIDTH );
8287
8388 text.reset ();
84- }
89+ rect.reset ();
90+ layout.reset ();
91+ }
92+
93+ TEST (Element, textSideBySide) {
94+ Tests::Tricks::createBackendSupport ();
95+
96+ const CBox POSITION = {0 , 0 , 300 , 100 };
97+
98+ auto text1 = CTextBuilder::begin ()
99+ ->size ({CDynamicSize::HT_SIZE_PERCENT , CDynamicSize::HT_SIZE_AUTO , {0.5 , 1.0 }})
100+ ->text (" First longish paragraph goes here. I love Hyprland it is the best." )
101+ ->noEllipsize (true )
102+ ->commence ();
103+ auto text2 = CTextBuilder::begin ()
104+ ->size ({CDynamicSize::HT_SIZE_PERCENT , CDynamicSize::HT_SIZE_AUTO , {0.5 , 1.0 }})
105+ ->text (" Second longish paragraph goes here. Who needs GTK and Qt when you have Hyprtoolkit?" )
106+ ->noEllipsize (true )
107+ ->commence ();
108+ auto layout = CRowLayoutBuilder::begin ()->commence ();
109+
110+ const auto NATURAL_HEIGHT = text1->preferredSize ({0 , 0 })->y ;
111+
112+ layout->addChild (text1);
113+ layout->addChild (text2);
114+
115+ g_positioner->position (layout, POSITION );
116+
117+ ASSERT_EQ (layout->impl ->position .width , POSITION .width );
118+ ASSERT_EQ (text1->impl ->position .width , POSITION .width / 2 );
119+ ASSERT_EQ (text2->impl ->position .width , POSITION .width / 2 );
120+ ASSERT_EQ (text1->impl ->position .width , text2->impl ->position .x );
121+ ASSERT_GT (text1->impl ->position .height , NATURAL_HEIGHT );
122+ ASSERT_GT (text2->impl ->position .height , NATURAL_HEIGHT );
123+
124+ text1.reset ();
125+ text2.reset ();
126+ layout.reset ();
127+ }
128+
129+ TEST (Element, textShrinkingThenGrowing) {
130+ Tests::Tricks::createBackendSupport ();
131+
132+ // This container is necessary because we need the text to auto size itself
133+ // if we position it directly, the text sizing code is never run
134+ auto container = CNullBuilder::begin ()->commence ();
135+ auto text = CTextBuilder::begin ()->text (" First longish paragraph goes here. I love Hyprland it is the best." )->commence ();
136+
137+ container->addChild (text);
138+
139+ const auto NATURAL_WIDTH = text->preferredSize ({0 , 0 })->x ;
140+ const auto CLAMPED_WIDTH = std::floor (NATURAL_WIDTH * 3 / 4 );
141+
142+ g_positioner->position (container, {0 , 0 , CLAMPED_WIDTH , 100 });
143+ ASSERT_LE (text->impl ->position .width , CLAMPED_WIDTH );
144+
145+ g_positioner->position (container, {0 , 0 , NATURAL_WIDTH * 2 , 100 });
146+ ASSERT_EQ (text->impl ->position .width , NATURAL_WIDTH );
147+
148+ container.reset ();
149+ text.reset ();
150+ }
151+
152+ TEST (Element, textChangingSize) {
153+ Tests::Tricks::createBackendSupport ();
154+
155+ auto text = CTextBuilder::begin ()->text (" First longish paragraph goes here. I love Hyprland it is the best." )->commence ();
156+
157+ const auto FIRST_SIZE = text->preferredSize ({0 , 0 });
158+
159+ text->rebuild ()->fontSize (CFontSize::HT_FONT_H1 )->commence ();
160+
161+ const auto SECOND_SIZE = text->preferredSize ({0 , 0 });
162+
163+ ASSERT_GT (SECOND_SIZE ->x , FIRST_SIZE ->x );
164+ ASSERT_GT (SECOND_SIZE ->y , FIRST_SIZE ->y );
165+
166+ text.reset ();
167+ }
168+
169+ TEST (Element, textChangingContent) {
170+ Tests::Tricks::createBackendSupport ();
171+
172+ auto text = CTextBuilder::begin ()->text (" First longish paragraph goes here." )->commence ();
173+
174+ const auto FIRST_SIZE = text->preferredSize ({0 , 0 });
175+
176+ text->rebuild ()->text (" First longish paragraph goes here but event longer" )->commence ();
177+
178+ const auto SECOND_SIZE = text->preferredSize ({0 , 0 });
179+
180+ ASSERT_GT (SECOND_SIZE ->x , FIRST_SIZE ->x );
181+
182+ text.reset ();
183+ }
184+
185+ TEST (Element, textClampSize) {
186+ Tests::Tricks::createBackendSupport ();
187+
188+ auto text = CTextBuilder::begin ()->text (" First longish paragraph goes here." )->commence ();
189+
190+ const auto NATURAL_SIZE = text->preferredSize ({0 , 0 });
191+ const auto CLAMPED_WIDTH = std::floor (NATURAL_SIZE ->x * 3 / 4 );
192+
193+ text->rebuild ()->clampSize ({CLAMPED_WIDTH , -1.0 })->commence ();
194+
195+ ASSERT_LE (text->preferredSize ({0 , 0 })->x , CLAMPED_WIDTH );
196+
197+ text.reset ();
198+ }
199+
200+ TEST (Element, textCharPosition) {
201+ Tests::Tricks::createBackendSupport ();
202+
203+ auto text = CTextBuilder::begin ()->text (" First longish paragraph goes here." )->commence ();
204+
205+ ASSERT_EQ (text->m_impl ->getCursorPos (0 ), 0 );
206+ ASSERT_GT (text->m_impl ->getCursorPos (1 ), 0 );
207+ ASSERT_GT (text->m_impl ->getCursorPos (1000 ), 0 );
208+
209+ text->rebuild ()->text (" " )->commence ();
210+
211+ ASSERT_EQ (text->m_impl ->getCursorPos (0 ), 0 );
212+ ASSERT_EQ (text->m_impl ->getCursorPos (1 ), 0 );
213+
214+ text.reset ();
215+ }
0 commit comments