Skip to content

Commit ed2529a

Browse files
committed
Fixed observed range rendering for shared_ptrs.
1 parent e1cd217 commit ed2529a

3 files changed

Lines changed: 92 additions & 7 deletions

File tree

nui/include/nui/event_system/range.hpp

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,10 @@ namespace Nui
9090
using BasicObservedRange<ObservedRange<ObservedValue>>::before_;
9191
using BasicObservedRange<ObservedRange<ObservedValue>>::after_;
9292

93-
static constexpr bool isRandomAccess = ObservedType::isRandomAccess;
93+
// Unwrap through shared_ptr/weak_ptr<Observed> so the trait reads from the
94+
// underlying Observed rather than the smart pointer (which has no isRandomAccess).
95+
static constexpr bool isRandomAccess =
96+
Detail::ObservedAddMutableReference_raw<ObservedType>::isRandomAccess;
9497

9598
template <typename ObservedValueT>
9699
requires std::is_same_v<std::decay_t<ObservedValueT>, std::decay_t<ObservedValueT>>
@@ -166,7 +169,24 @@ namespace Nui
166169
return ObservedRange<const ObservedValue>{observedValues};
167170
}
168171

172+
/**
173+
* @brief Reactive range over a shared_ptr/weak_ptr<Observed<Container>>.
174+
*
175+
* The smart pointer is stored internally as a weak_ptr (matching how shared/weak
176+
* observed are treated elsewhere), locked per update, so the range reacts to
177+
* mutations and reassignment of the underlying Observed. The caller must keep the
178+
* shared_ptr alive for as long as the rendered range is mounted. Taken by value so
179+
* the weak_ptr conversion is well-formed for both shared_ptr and weak_ptr inputs.
180+
*/
181+
template <typename ObservedValue>
182+
requires(IsSharedObserved<ObservedValue> || IsWeakObserved<ObservedValue>)
183+
ObservedRange<std::decay_t<ObservedValue>> range(ObservedValue observedValues)
184+
{
185+
return ObservedRange<std::decay_t<ObservedValue>>{std::move(observedValues)};
186+
}
187+
169188
template <typename ContainerT, typename... Observed>
189+
requires(!IsObservedLike<std::remove_cvref_t<ContainerT>>)
170190
UnoptimizedRange<IteratorAccessor<ContainerT const>, std::decay_t<Detail::ObservedAddReference_t<Observed>>...>
171191
range(ContainerT const& container, Observed&&... observed)
172192
{
@@ -179,6 +199,7 @@ namespace Nui
179199
}
180200

181201
template <typename ContainerT, typename... Observed>
202+
requires(!IsObservedLike<std::remove_cvref_t<ContainerT>>)
182203
UnoptimizedRange<IteratorAccessor<ContainerT>, std::decay_t<Detail::ObservedAddReference_t<Observed>>...>
183204
range(ContainerT& container, Observed&&... observed)
184205
{
@@ -237,6 +258,7 @@ namespace Nui
237258
}
238259

239260
template <typename ContainerT>
261+
requires(!IsObservedLike<std::remove_cvref_t<ContainerT>>)
240262
UnoptimizedRange<IteratorAccessor<ContainerT const>> range(ContainerT const& container)
241263
{
242264
return UnoptimizedRange<IteratorAccessor<ContainerT const>>{
@@ -250,7 +272,7 @@ namespace Nui
250272
* even if the caller's local container goes out of scope immediately.
251273
*/
252274
template <typename ContainerT>
253-
requires(!IsObserved<std::remove_cvref_t<ContainerT>>)
275+
requires(!IsObservedLike<std::remove_cvref_t<ContainerT>>)
254276
UnoptimizedRange<OwningIteratorAccessor<std::remove_cvref_t<ContainerT>>>
255277
range(ContainerT&& container)
256278
{
@@ -262,6 +284,7 @@ namespace Nui
262284

263285
#ifdef NUI_HAS_STD_RANGES
264286
template <typename T, typename... Observed>
287+
requires(!IsObservedLike<std::remove_cvref_t<T>>)
265288
UnoptimizedRange<
266289
std::ranges::subrange<std::ranges::iterator_t<T const>>,
267290
std::decay_t<Detail::ObservedAddReference_t<Observed>>...> range(T const& container, Observed&&... observed)

nui/include/nui/frontend/elements/impl/range_renderer.tpp

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,10 @@ namespace Nui::Detail
6161
return &valueRange;
6262
},
6363
[&holder](::Nui::IsWeakObserved auto& valueRange) {
64-
if (holder.locked = valueRange.lock(); holder.locked)
65-
return holder.locked.get();
66-
return nullptr;
64+
holder.locked = valueRange.lock();
65+
// get() yields nullptr when the weak_ptr could not be locked; both
66+
// overloads must return the same Observed* type for auto deduction.
67+
return holder.locked.get();
6768
},
6869
}(valueRange_);
6970
}
@@ -208,8 +209,7 @@ namespace Nui::Detail
208209
if (!parent)
209210
return InvalidateRange;
210211

211-
auto valueRangeHolder =
212-
::Nui::Detail::HoldToken<std::decay_t<ObservedAddMutableReference_t<typename RangeT::ObservedType>>>{};
212+
auto valueRangeHolder = CommonHoldToken<RangeT>{};
213213
auto* valueRange = getValueRange(valueRangeHolder);
214214
if (!valueRange)
215215
return InvalidateRange;

nui/test/nui/test_ranges.hpp

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,68 @@ namespace Nui::Tests
5050
textBodyParityTest(vec, parent);
5151
}
5252

53+
// range() over a shared_ptr<Observed<...>> must be reactive (stored as a weak_ptr
54+
// internally, locked per update). Reassigning the underlying Observed re-renders.
55+
TEST_F(TestRanges, SharedPtrObservedReassignmentUpdatesView)
56+
{
57+
using Nui::Elements::div;
58+
using Nui::Elements::body;
59+
using namespace Nui::Attributes;
60+
61+
Nui::val parent;
62+
auto vec = std::make_shared<Observed<std::vector<char>>>(std::vector<char>{'A', 'B', 'C', 'D'});
63+
64+
render(body{reference = parent}(range(vec), [](long long, auto const& element) {
65+
return div{}(std::string{element});
66+
}));
67+
textBodyParityTest(*vec, parent);
68+
69+
*vec = std::vector<char>{'X', 'Y', 'Z'};
70+
globalEventContext.executeActiveEventsImmediately();
71+
textBodyParityTest(*vec, parent);
72+
}
73+
74+
// Element-level mutation (push_back) through the shared_ptr also re-renders.
75+
TEST_F(TestRanges, SharedPtrObservedPushBackUpdatesView)
76+
{
77+
using Nui::Elements::div;
78+
using Nui::Elements::body;
79+
using namespace Nui::Attributes;
80+
81+
Nui::val parent;
82+
auto vec = std::make_shared<Observed<std::vector<char>>>(std::vector<char>{'A', 'B'});
83+
84+
render(body{reference = parent}(range(vec), [](long long, auto const& element) {
85+
return div{}(std::string{element});
86+
}));
87+
textBodyParityTest(*vec, parent);
88+
89+
vec->push_back('C');
90+
globalEventContext.executeActiveEventsImmediately();
91+
textBodyParityTest(*vec, parent);
92+
}
93+
94+
// weak_ptr<Observed<...>> is accepted too; it reacts while the owner keeps it alive.
95+
TEST_F(TestRanges, WeakPtrObservedUpdatesView)
96+
{
97+
using Nui::Elements::div;
98+
using Nui::Elements::body;
99+
using namespace Nui::Attributes;
100+
101+
Nui::val parent;
102+
auto vec = std::make_shared<Observed<std::vector<char>>>(std::vector<char>{'A', 'B', 'C', 'D'});
103+
std::weak_ptr<Observed<std::vector<char>>> weak = vec;
104+
105+
render(body{reference = parent}(range(weak), [](long long, auto const& element) {
106+
return div{}(std::string{element});
107+
}));
108+
textBodyParityTest(*vec, parent);
109+
110+
vec->push_back('E');
111+
globalEventContext.executeActiveEventsImmediately();
112+
textBodyParityTest(*vec, parent);
113+
}
114+
53115
TEST_F(TestRanges, PushBackUpdatesView)
54116
{
55117
Nui::val parent;

0 commit comments

Comments
 (0)