Skip to content

Commit 691ae28

Browse files
authored
Adoption (#660)
* adoption * hopefully fixed broken AUI_DECLARATIVE_FOR * update Devtools::prettyName * fix android and ios * u * fix tests
1 parent 74d178e commit 691ae28

File tree

13 files changed

+5272
-211
lines changed

13 files changed

+5272
-211
lines changed

aui.core/src/AUI/Common/AUuid.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,14 @@ inline std::ostream& operator<<(std::ostream& o, const AUuid& u) {
106106
return o;
107107
}
108108

109+
template <>
110+
struct fmt::formatter<AUuid> : fmt::formatter<AString> {
111+
template <typename FormatContext>
112+
auto format(const AUuid& uuid, FormatContext& ctx) const {
113+
return fmt::formatter<AString>::format(uuid.toString(), ctx);
114+
}
115+
};
116+
109117

110118
class AUuidException: public AException {
111119
private:

aui.core/src/AUI/Hash.h

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// AUI Framework - Declarative UI toolkit for modern C++20
2+
// Copyright (C) 2020-2025 Alex2772 and Contributors
3+
//
4+
// SPDX-License-Identifier: MPL-2.0
5+
//
6+
// This Source Code Form is subject to the terms of the Mozilla Public
7+
// License, v. 2.0. If a copy of the MPL was not distributed with this
8+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
9+
10+
#pragma once
11+
12+
#include <map>
13+
14+
namespace aui {
15+
16+
namespace hash_detail {
17+
18+
template <std::size_t Bits>
19+
struct hash_mix_impl;
20+
21+
// hash_mix for 64 bit size_t
22+
//
23+
// The general "xmxmx" form of state of the art 64 bit mixers originates
24+
// from Murmur3 by Austin Appleby, which uses the following function as
25+
// its "final mix":
26+
//
27+
// k ^= k >> 33;
28+
// k *= 0xff51afd7ed558ccd;
29+
// k ^= k >> 33;
30+
// k *= 0xc4ceb9fe1a85ec53;
31+
// k ^= k >> 33;
32+
//
33+
// (https://github.com/aappleby/smhasher/blob/master/src/MurmurHash3.cpp)
34+
//
35+
// It has subsequently been improved multiple times by different authors
36+
// by changing the constants. The most well known improvement is the
37+
// so-called "variant 13" function by David Stafford:
38+
//
39+
// k ^= k >> 30;
40+
// k *= 0xbf58476d1ce4e5b9;
41+
// k ^= k >> 27;
42+
// k *= 0x94d049bb133111eb;
43+
// k ^= k >> 31;
44+
//
45+
// (https://zimbry.blogspot.com/2011/09/better-bit-mixing-improving-on.html)
46+
//
47+
// This mixing function is used in the splitmix64 RNG:
48+
// http://xorshift.di.unimi.it/splitmix64.c
49+
//
50+
// We use Jon Maiga's implementation from
51+
// http://jonkagstrom.com/mx3/mx3_rev2.html
52+
//
53+
// x ^= x >> 32;
54+
// x *= 0xe9846af9b1a615d;
55+
// x ^= x >> 32;
56+
// x *= 0xe9846af9b1a615d;
57+
// x ^= x >> 28;
58+
//
59+
// An equally good alternative is Pelle Evensen's Moremur:
60+
//
61+
// x ^= x >> 27;
62+
// x *= 0x3C79AC492BA7B653;
63+
// x ^= x >> 33;
64+
// x *= 0x1C69B3F74AC4AE35;
65+
// x ^= x >> 27;
66+
//
67+
// (https://mostlymangling.blogspot.com/2019/12/stronger-better-morer-moremur-better.html)
68+
69+
template <>
70+
struct hash_mix_impl<64> {
71+
inline static std::uint64_t fn(std::uint64_t x) {
72+
std::uint64_t const m = (static_cast<std::uint64_t>(0xe9846af) << 32) + 0x9b1a615d;
73+
74+
x ^= x >> 32;
75+
x *= m;
76+
x ^= x >> 32;
77+
x *= m;
78+
x ^= x >> 28;
79+
80+
return x;
81+
}
82+
};
83+
84+
// hash_mix for 32 bit size_t
85+
//
86+
// We use the "best xmxmx" implementation from
87+
// https://github.com/skeeto/hash-prospector/issues/19
88+
89+
template <>
90+
struct hash_mix_impl<32> {
91+
inline static std::uint32_t fn(std::uint32_t x) {
92+
std::uint32_t const m1 = 0x21f0aaad;
93+
std::uint32_t const m2 = 0x735a2d97;
94+
95+
x ^= x >> 16;
96+
x *= m1;
97+
x ^= x >> 15;
98+
x *= m2;
99+
x ^= x >> 15;
100+
101+
return x;
102+
}
103+
};
104+
105+
inline std::size_t hash_mix(std::size_t v) { return hash_mix_impl<sizeof(std::size_t) * CHAR_BIT>::fn(v); }
106+
107+
} // namespace hash_detail
108+
109+
//
110+
// boost::hash_combine
111+
//
112+
113+
template <class T>
114+
inline void hash_combine(std::size_t& seed, T const& v) {
115+
seed = aui::hash_detail::hash_mix(seed + 0x9e3779b9 + std::hash<T>()(v));
116+
}
117+
118+
namespace hash_detail {
119+
template <class It>
120+
inline std::size_t hash_range(std::size_t seed, It first, It last) {
121+
for (; first != last; ++first) {
122+
hash_combine<typename std::iterator_traits<It>::value_type>(seed, *first);
123+
}
124+
125+
return seed;
126+
}
127+
}
128+
129+
//
130+
// boost::hash_range
131+
//
132+
133+
template <class It>
134+
inline void hash_range(std::size_t& seed, It first, It last) {
135+
seed = hash_detail::hash_range(seed, first, last);
136+
}
137+
138+
template <class It>
139+
inline std::size_t hash_range(It first, It last) {
140+
std::size_t seed = 0;
141+
142+
hash_range(seed, first, last);
143+
144+
return seed;
145+
}
146+
} // namespace aui

aui.uitests/tests/UIDeclarativeForTest.cpp

Lines changed: 109 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -379,10 +379,7 @@ TEST_F(UIDeclarativeForTest, Reactive_lists) { // HEADER_H3
379379
}
380380

381381
TEST_F(UIDeclarativeForTest, DynamicPerformance) {
382-
struct State {
383-
AProperty<AVector<AString>> items = AVector<AString> { "Hello", "World", "Test" };
384-
};
385-
auto state = _new<State>();
382+
386383

387384
::testing::GTEST_FLAG(throw_on_failure) = true;
388385

@@ -392,18 +389,72 @@ TEST_F(UIDeclarativeForTest, DynamicPerformance) {
392389
EXPECT_CALL(mTestObserver, onViewCreated("Test"_as));
393390
EXPECT_CALL(mTestObserver, onViewCreated("Bruh"_as));
394391

392+
// ## View caching { #AFOREACHUI_CACHING }
393+
//
394+
// By default, AForEachUI does not cache instantiated views. This means that every time the underlying model is
395+
// changed, all views are destroyed and recreated. This is not efficient in many scenarios, especially when views are
396+
// complex and model changes are small.
397+
//
398+
// To enable view caching, you need to provide a *key function* that generates a unique key for each item in the
399+
// model. The key function is a lambda that takes an item and returns a `std::size_t` hash of the value.
400+
//
401+
// AUI_DOCS_CODE_BEGIN
402+
struct State {
403+
AProperty<AVector<AString>> items = AVector<AString> { "Hello", "World", "Test" };
404+
};
405+
auto state = _new<State>();
406+
407+
auto testObserver = &mTestObserver;
408+
395409
mWindow->setContents(Vertical {
396410
AScrollArea::Builder()
397411
.withContents(
398-
AUI_DECLARATIVE_FOR_EX(i, *state->items, AVerticalLayout, &) {
399-
mTestObserver.onViewCreated(i);
412+
AUI_DECLARATIVE_FOR(i, *state->items, AVerticalLayout) {
413+
testObserver->onViewCreated(i); // HIDE
414+
ALogger::info("Test") << "Created view: " << i;
400415
return Label { i };
416+
} AUI_LET {
417+
it->setKeyFunction([](const AString& k) {
418+
return std::hash<AString>{}(k);
419+
});
401420
})
402421
.build() AUI_OVERRIDE_STYLE { FixedSize { 150_dp, 200_dp } },
403422
});
423+
// AUI_DOCS_CODE_END
424+
saveScreenshot("1");
425+
//
426+
// <figure markdown="span">
427+
// ![](imgs/UIDeclarativeForTest.DynamicPerformance_1.png){ width="500" }
428+
// <figcaption>Basic view caching example.</figcaption>
429+
// </figure>
430+
//
431+
//
432+
// ``` title="Possible output"
433+
// [07:09:06][][Test][INFO]: Created view: Hello
434+
// [07:09:06][][Test][INFO]: Created view: World
435+
// [07:09:06][][Test][INFO]: Created view: Test
436+
// ```
404437

405438
uitest::frame();
439+
//
440+
// Upon adding a new item to the model, only the new item's view is created. The existing views are reused from
441+
// the cache.
442+
// AUI_DOCS_CODE_BEGIN
406443
state->items.writeScope()->push_back("Bruh");
444+
// AUI_DOCS_CODE_END
445+
//
446+
// If we were not using view caching, all views would be recreated:
447+
// ``` title="Possible output without view caching"
448+
// [07:09:06][][Test][INFO]: Created view: Hello
449+
// [07:09:06][][Test][INFO]: Created view: World
450+
// [07:09:06][][Test][INFO]: Created view: Test
451+
// [07:09:10][][Test][INFO]: Created view: Hello
452+
// [07:09:10][][Test][INFO]: Created view: World
453+
// [07:09:10][][Test][INFO]: Created view: Test
454+
// [07:09:10][][Test][INFO]: Created view: Bruh
455+
// ```
456+
//
457+
407458
uitest::frame();
408459
EXPECT_EQ(cache<AForEachUI<AString>>().size(), 0);
409460
}
@@ -446,6 +497,12 @@ TEST_F(UIDeclarativeForTest, IntGrouping) {
446497
TEST_F(UIDeclarativeForTest, IntGroupingDynamic1) {
447498
::testing::GTEST_FLAG(throw_on_failure) = true;
448499

500+
//
501+
// ### Advanced grouping with caching
502+
//
503+
// In this example, we demonstrate a more advanced use case of AForEachUI with nested grouping and view caching.
504+
//
505+
// AUI_DOCS_CODE_BEGIN
449506
struct State {
450507
AProperty<AVector<int>> ints = AVector<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 };
451508
};
@@ -471,11 +528,49 @@ TEST_F(UIDeclarativeForTest, IntGroupingDynamic1) {
471528
auto str = "{}"_format(i);
472529
mTestObserver.onViewCreated(str);
473530
return Label { std::move(str) };
531+
} AUI_LET {
532+
it->setKeyFunction([](int k) {
533+
return k;
534+
});
474535
}
475536
};
537+
} AUI_LET {
538+
it->setKeyFunction([](const auto& rng) {
539+
return aui::hash_range(ranges::begin(rng), ranges::end(rng));;
540+
});
476541
})
477542
.build() AUI_OVERRIDE_STYLE { FixedSize { 150_dp, 300_dp } },
478543
});
544+
// AUI_DOCS_CODE_END
545+
//
546+
// In this example, we have a nested AForEachUI structure, where the outer AForEachUI iterates over groups of
547+
// integers, and the inner AForEachUI iterates over individual integers within each group.
548+
//
549+
// <figure markdown="span">
550+
// ![](imgs/UIDeclarativeForTest.IntGroupingDynamic1_1.png){ width="500" }
551+
// <figcaption>Advanced grouping example.</figcaption>
552+
// </figure>
553+
//
554+
// In addition to user-provided `setKeyFunction` outcome, the key is strengthened with byte-level hash of the item
555+
// by the framework. This is required to enforce uniqueness of `ranges::view::chunk` which stores iterator pair to
556+
// a generic container.
557+
//
558+
// ??? "More about byte-level hashing"
559+
//
560+
// The goal is to avoid incorrect cache hits (reusing a view for the “wrong” element or group), especially for
561+
// tricky range types like `ranges::views::chunk` that store iterator pairs and are sensitive to container
562+
// mutations.
563+
//
564+
// The subrange object (e.g., from `ranges::views::chunk`) contains iterators pointing to the original
565+
// container. If the container is modified (elements added/removed), these iterators may become invalid or
566+
// shifted. Since cached view captures the non-owning subrange object by value, iterating over it later may lead
567+
// to unexpected results.
568+
//
569+
// The raw byte-level hash helps to identify underlying iterator changes that may not be reflected in
570+
// user-provided keys, preventing reuse of the stale cached views.
571+
//
572+
// The hash enforced by the framework can be customized by specializing `aui::for_each_ui::defaultKey<T>`.
573+
//
479574

480575
EXPECT_CALL(mTestObserver, onViewCreated("Group 0"_as));
481576
EXPECT_CALL(mTestObserver, onViewCreated("1"_as));
@@ -502,7 +597,14 @@ TEST_F(UIDeclarativeForTest, IntGroupingDynamic1) {
502597
/* Also, depending on used container's iterator implementation, other groups might be evaluated as well. In our
503598
* case, we are using AVector, whose iterator is an offset from beginning. Since the offset has changed due to
504599
* removal, the iterator is considered dirty. This might not be the case for containers whose items are stored in
505-
* heap, i.e., `std::list`. */
600+
* heap, i.e., `std::list`.
601+
*
602+
* We need to be especially careful with this. Since we are using ranges::view::chunk in our example, it stores
603+
* iterator pair to std::vector. Iterators of std::vector MUST BE invalidated since we have changed the container.
604+
*
605+
* Given that, we expect "Group 10" to be reevaluated, despite user-provided setKeyFunction outcome would give equal
606+
* hashes both before and after removal.
607+
*/
506608
EXPECT_CALL(mTestObserver, onViewCreated("Group 10"_as));
507609
{
508610
state->ints.writeScope()->removeAll(2);

aui.views/src/AUI/Devtools/Devtools.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ AString Devtools::prettyViewName(AView* view) {
4141
}
4242
auto filter = [](const AString& name) { return !name.contains("::detail::"); };
4343
if (auto rng = view->getAssNames() | ranges::view::filter(filter); !ranges::empty(rng)) {
44-
name = ranges::back(rng);
44+
name += " " + ranges::back(rng);
4545
}
4646
return name;
4747
}

aui.views/src/AUI/Platform/android/OpenGLRenderingContextImpl.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@
2222

2323
#include <AUI/GL/OpenGLRenderer.h>
2424
#include <AUI/GL/State.h>
25-
25+
#include <EGL/egl.h>
2626

2727
void OpenGLRenderingContext::init(const Init& init) {
2828
CommonRenderingContext::init(init);
29+
gladLoadGLES2Loader(reinterpret_cast<GLADloadproc>(eglGetProcAddress));
2930
mRenderer = ourRenderer();
3031
}
3132

aui.views/src/AUI/Platform/ios/OpenGLRenderingContextImpl.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// Created by Alex2772 on 12/7/2021.
1414
//
1515

16+
#include <dlfcn.h>
1617
#include <AUI/GL/gl.h>
1718
#include <AUI/Platform/OpenGLRenderingContext.h>
1819
#include <AUI/Util/ARandom.h>
@@ -23,9 +24,18 @@
2324
#include <AUI/GL/OpenGLRenderer.h>
2425
#include <AUI/GL/State.h>
2526

27+
static void* UIKit_GL_GetProcAddress(const char *proc)
28+
{
29+
/* Look through all SO's for the proc symbol. Here's why:
30+
* -Looking for the path to the OpenGL Library seems not to work in the iOS Simulator.
31+
* -We don't know that the path won't change in the future. */
32+
return dlsym(RTLD_DEFAULT, proc);
33+
}
34+
2635

2736
void OpenGLRenderingContext::init(const Init& init) {
2837
CommonRenderingContext::init(init);
38+
gladLoadGLES2Loader(reinterpret_cast<GLADloadproc>(UIKit_GL_GetProcAddress));
2939
mRenderer = ourRenderer();
3040
}
3141

aui.views/src/AUI/Platform/linux/x11/PlatformAbstractionX11.cpp

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -485,8 +485,7 @@ void PlatformAbstractionX11::windowSetGeometry(AWindow& window, int x, int y, in
485485
if (!nativeHandle(window))
486486
return;
487487
WindowStyle style = window.windowStyle();
488-
bool isPopupMenu = (style == WindowStyle::DEFAULT) ||
489-
((style & WindowStyle::SYS) == WindowStyle::SYS);
488+
bool isPopupMenu = (style & WindowStyle::SYS) == WindowStyle::SYS;
490489

491490
XWindowChanges changes;
492491
changes.x = x;

0 commit comments

Comments
 (0)