Files
khodowany 522254dfc1
publish / publish (push) Successful in 14m40s
Cleanup
2026-09-24 19:43:50 -05:00

2487 lines
83 KiB
C++

#include <algorithm>
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <expected>
#include <limits>
#include <memory>
#include <mutex>
#include <numeric>
#include <stdexcept>
#include <string>
#include <string_view>
#include <thread>
#include <utility>
#include <vector>
#include <boost/json/parse.hpp>
#include <modulus.hpp>
#include <luma.hpp>
#include <test.hpp>
namespace {
struct fake_draw_context final : public luma::draw_context {
auto fill_rect(luma::rect bounds, luma::color) -> void override;
auto stroke_rect(luma::rect, std::float32_t, luma::color) -> void override;
auto stroke_line(luma::point, luma::point, std::float32_t, luma::color) -> void override;
auto fill_ellipse(luma::rect, luma::color) -> void override;
auto stroke_ellipse(luma::rect, std::float32_t, luma::color) -> void override;
auto draw_text(std::string_view, luma::point origin, luma::text_style const &style, luma::color) -> void override;
auto draw_texture(luma::texture const &, luma::rect, luma::rect, luma::color) -> void override;
auto push_clip(luma::rect bounds) -> void override;
auto pop_clip() -> void override;
auto measure_text(std::string_view text, luma::text_style const &style) const -> luma::size override;
std::vector<luma::rect> filled{};
std::vector<luma::rect> clips{};
std::vector<luma::text_style> drawn_styles{};
std::vector<luma::point> drawn_origins{};
std::int32_t clip_depth{};
std::int32_t max_clip_depth{};
};
class fixed_control final : public luma::control {
public:
explicit fixed_control(luma::size value);
auto measure(luma::draw_context &, luma::theme const &, luma::constraints) -> luma::size override;
auto arrange(luma::rect bounds) -> void override;
auto draw(luma::draw_context &, luma::theme const &) const -> void override;
auto handle_touch(luma::touch_event const &event) -> std::bool_t override;
luma::size value_{};
luma::rect arranged{};
std::bool_t touched{false};
luma::touch_phase last_phase{luma::touch_phase::MOVE};
};
struct counting_control final : public luma::control {
explicit counting_control(luma::size value);
auto measure(luma::draw_context &, luma::theme const &, luma::constraints) -> luma::size override;
auto arrange(luma::rect bounds) -> void override;
auto draw(luma::draw_context &, luma::theme const &) const -> void override;
auto handle_touch(luma::touch_event const &event) -> std::bool_t override;
auto invalidate_layout() -> void override;
luma::size value_{};
luma::rect arranged{};
std::int32_t measure_count{};
std::int32_t arrange_count{};
std::int32_t invalidate_count{};
};
struct filling_control final : public luma::control {
auto measure(luma::draw_context &, luma::theme const &, luma::constraints limits) -> luma::size override;
auto arrange(luma::rect bounds) -> void override;
auto draw(luma::draw_context &context, luma::theme const &) const -> void override;
luma::rect arranged{};
std::int32_t measure_count{};
};
class fake_texture final : public luma::texture {
public:
fake_texture(std::int32_t width = 16, std::int32_t height = 16);
auto info() const -> luma::texture_info override;
private:
luma::texture_info info_{};
};
struct fake_resources final : public luma::renderer_resources {
auto load_texture(std::string const &) -> std::expected<std::shared_ptr<luma::texture>, modulus::error> override;
};
struct reentrant_resources final : public luma::renderer_resources {
auto load_texture(std::string const &path) -> std::expected<std::shared_ptr<luma::texture>, modulus::error> override;
luma::asset_cache *cache{};
std::int32_t load_count{};
std::bool_t inner_loaded{false};
};
struct same_path_reentrant_resources final : public luma::renderer_resources {
auto load_texture(std::string const &path) -> std::expected<std::shared_ptr<luma::texture>, modulus::error> override;
luma::asset_cache *cache{};
std::int32_t load_count{};
std::bool_t reentered{false};
std::bool_t recursive_failed{false};
std::int32_t recursive_code{};
};
class blocking_resources final : public luma::renderer_resources {
public:
auto load_texture(std::string const &) -> std::expected<std::shared_ptr<luma::texture>, modulus::error> override;
auto wait_for_loads(std::int32_t count) -> std::bool_t;
auto release() -> void;
auto load_count() const -> std::int32_t;
private:
mutable std::mutex mutex_{};
std::condition_variable load_entered_{};
std::condition_variable release_load_{};
std::bool_t released_{false};
std::int32_t load_count_{};
};
struct recording_control final : public luma::control {
auto measure(luma::draw_context &, luma::theme const &, luma::constraints) -> luma::size override;
auto draw(luma::draw_context &, luma::theme const &) const -> void override;
auto handle_touch(luma::touch_event const &event) -> std::bool_t override;
std::vector<luma::touch_phase> phases{};
std::vector<std::int32_t> ids{};
};
struct lifetime_state {
std::bool_t in_handle{false};
std::bool_t destroyed{false};
std::bool_t destroyed_while_active{false};
std::bool_t status_ok{false};
std::bool_t visible_during_callback{false};
std::uint64_t opened_id{};
};
enum class close_phase {
MEASURE,
ARRANGE,
DRAW,
TOUCH,
INVALIDATE,
};
class destroy_probe_control final : public luma::control {
public:
explicit destroy_probe_control(std::shared_ptr<lifetime_state> state);
~destroy_probe_control() override;
auto measure(luma::draw_context &, luma::theme const &, luma::constraints) -> luma::size override;
auto draw(luma::draw_context &, luma::theme const &) const -> void override;
private:
std::shared_ptr<lifetime_state> state_{};
};
class closing_window_control final : public luma::control {
public:
closing_window_control(luma::window_manager &manager, std::uint64_t &id, std::shared_ptr<lifetime_state> state);
~closing_window_control() override;
auto measure(luma::draw_context &, luma::theme const &, luma::constraints) -> luma::size override;
auto draw(luma::draw_context &, luma::theme const &) const -> void override;
auto handle_touch(luma::touch_event const &) -> std::bool_t override;
private:
luma::window_manager &manager_;
std::uint64_t &id_;
std::shared_ptr<lifetime_state> state_{};
};
class opening_window_control final : public luma::control {
public:
opening_window_control(luma::window_manager &manager, std::shared_ptr<lifetime_state> state);
auto measure(luma::draw_context &, luma::theme const &, luma::constraints) -> luma::size override;
auto draw(luma::draw_context &, luma::theme const &) const -> void override;
auto handle_touch(luma::touch_event const &) -> std::bool_t override;
private:
luma::window_manager &manager_;
std::shared_ptr<lifetime_state> state_{};
};
class updating_window_control final : public luma::control {
public:
updating_window_control(luma::window_manager &manager, std::uint64_t &id, std::shared_ptr<lifetime_state> state);
auto measure(luma::draw_context &, luma::theme const &, luma::constraints) -> luma::size override;
auto draw(luma::draw_context &, luma::theme const &) const -> void override;
auto handle_touch(luma::touch_event const &) -> std::bool_t override;
private:
luma::window_manager &manager_;
std::uint64_t &id_;
std::shared_ptr<lifetime_state> state_{};
};
class replacing_root_invalidate_control final : public luma::control {
public:
replacing_root_invalidate_control(luma::application &app, std::shared_ptr<lifetime_state> state);
~replacing_root_invalidate_control() override;
auto measure(luma::draw_context &, luma::theme const &, luma::constraints) -> luma::size override;
auto draw(luma::draw_context &, luma::theme const &) const -> void override;
auto invalidate_layout() -> void override;
private:
luma::application &app_;
std::shared_ptr<lifetime_state> state_{};
};
class closing_window_invalidate_control final : public luma::control {
public:
closing_window_invalidate_control(luma::window_manager &manager, std::uint64_t &id, std::shared_ptr<lifetime_state> state);
~closing_window_invalidate_control() override;
auto measure(luma::draw_context &, luma::theme const &, luma::constraints) -> luma::size override;
auto draw(luma::draw_context &, luma::theme const &) const -> void override;
auto invalidate_layout() -> void override;
private:
luma::window_manager &manager_;
std::uint64_t &id_;
std::shared_ptr<lifetime_state> state_{};
};
class closing_dialog_control final : public luma::control {
public:
closing_dialog_control(luma::overlay_host &host, std::shared_ptr<lifetime_state> state, close_phase phase);
~closing_dialog_control() override;
auto measure(luma::draw_context &, luma::theme const &, luma::constraints) -> luma::size override;
auto arrange(luma::rect bounds) -> void override;
auto draw(luma::draw_context &, luma::theme const &) const -> void override;
auto handle_touch(luma::touch_event const &) -> std::bool_t override;
auto invalidate_layout() -> void override;
private:
auto close_if(close_phase phase) const -> void;
luma::overlay_host &host_;
std::shared_ptr<lifetime_state> state_{};
close_phase phase_{close_phase::TOUCH};
};
auto fake_draw_context::fill_rect(luma::rect bounds, luma::color) -> void {
filled.push_back(bounds);
}
auto fake_draw_context::stroke_rect(luma::rect, std::float32_t, luma::color) -> void {}
auto fake_draw_context::stroke_line(luma::point, luma::point, std::float32_t, luma::color) -> void {}
auto fake_draw_context::fill_ellipse(luma::rect, luma::color) -> void {}
auto fake_draw_context::stroke_ellipse(luma::rect, std::float32_t, luma::color) -> void {}
auto fake_draw_context::draw_text(std::string_view, luma::point origin, luma::text_style const &style, luma::color) -> void {
drawn_styles.push_back(style);
drawn_origins.push_back(origin);
}
auto fake_draw_context::draw_texture(luma::texture const &, luma::rect, luma::rect, luma::color) -> void {}
auto fake_draw_context::push_clip(luma::rect bounds) -> void {
clips.push_back(bounds);
++clip_depth;
max_clip_depth = std::max(max_clip_depth, clip_depth);
}
auto fake_draw_context::pop_clip() -> void {
--clip_depth;
}
auto fake_draw_context::measure_text(std::string_view text, luma::text_style const &style) const -> luma::size {
return {
.width = static_cast<std::float32_t>(text.size()) * style.size * std::float32_t{0.5F},
.height = style.line_height,
};
}
fixed_control::fixed_control(luma::size value)
: value_{value} {}
auto fixed_control::measure(luma::draw_context &, luma::theme const &, luma::constraints) -> luma::size {
return value_;
}
auto fixed_control::arrange(luma::rect bounds) -> void {
luma::control::arrange(bounds);
arranged = bounds;
}
auto fixed_control::draw(luma::draw_context &, luma::theme const &) const -> void {}
auto fixed_control::handle_touch(luma::touch_event const &event) -> std::bool_t {
for (auto const &point : event.points) {
if (luma::contains(bounds(), point.position)) {
touched = true;
last_phase = point.phase;
return true;
}
}
return false;
}
counting_control::counting_control(luma::size value)
: value_{value} {}
auto counting_control::measure(luma::draw_context &, luma::theme const &, luma::constraints) -> luma::size {
++measure_count;
return value_;
}
auto counting_control::arrange(luma::rect bounds) -> void {
luma::control::arrange(bounds);
arranged = bounds;
++arrange_count;
}
auto counting_control::draw(luma::draw_context &, luma::theme const &) const -> void {}
auto counting_control::handle_touch(luma::touch_event const &event) -> std::bool_t {
for (auto const &point : event.points) {
if (luma::contains(bounds(), point.position)) {
return true;
}
}
return false;
}
auto counting_control::invalidate_layout() -> void {
++invalidate_count;
}
auto filling_control::measure(luma::draw_context &, luma::theme const &, luma::constraints limits) -> luma::size {
++measure_count;
return luma::clamp({
.width = 20.0F,
.height = 20.0F,
},
limits);
}
auto filling_control::arrange(luma::rect bounds) -> void {
luma::control::arrange(bounds);
arranged = bounds;
}
auto filling_control::draw(luma::draw_context &context, luma::theme const &) const -> void {
context.fill_rect(bounds(), {});
}
fake_texture::fake_texture(std::int32_t width, std::int32_t height)
: info_{.width = width, .height = height} {}
auto fake_texture::info() const -> luma::texture_info {
return info_;
}
auto fake_resources::load_texture(std::string const &) -> std::expected<std::shared_ptr<luma::texture>, modulus::error> {
return modulus::success(std::make_shared<fake_texture>());
}
auto reentrant_resources::load_texture(std::string const &path) -> std::expected<std::shared_ptr<luma::texture>, modulus::error> {
++load_count;
if (path == "outer.png" && cache != nullptr) {
auto inner = cache->texture("inner.png");
inner_loaded = inner.has_value();
}
return modulus::success(std::make_shared<fake_texture>());
}
auto same_path_reentrant_resources::load_texture(std::string const &path) -> std::expected<std::shared_ptr<luma::texture>, modulus::error> {
++load_count;
if (!reentered && cache != nullptr) {
reentered = true;
auto recursive = cache->texture(path);
recursive_failed = !recursive;
if (!recursive) {
recursive_code = recursive.error().code();
}
}
return modulus::success(std::make_shared<fake_texture>());
}
auto blocking_resources::load_texture(std::string const &) -> std::expected<std::shared_ptr<luma::texture>, modulus::error> {
auto lock = std::unique_lock{mutex_};
++load_count_;
load_entered_.notify_all();
release_load_.wait(lock, [this] {
return released_;
});
return modulus::success(std::make_shared<fake_texture>());
}
auto blocking_resources::wait_for_loads(std::int32_t count) -> std::bool_t {
auto lock = std::unique_lock{mutex_};
return load_entered_.wait_for(lock, std::chrono::seconds{1}, [this, count] {
return load_count_ >= count;
});
}
auto blocking_resources::release() -> void {
auto lock = std::scoped_lock{mutex_};
released_ = true;
release_load_.notify_all();
}
auto blocking_resources::load_count() const -> std::int32_t {
auto lock = std::scoped_lock{mutex_};
return load_count_;
}
auto recording_control::measure(luma::draw_context &, luma::theme const &, luma::constraints) -> luma::size {
return {
.width = 20.0F,
.height = 20.0F,
};
}
auto recording_control::draw(luma::draw_context &, luma::theme const &) const -> void {}
auto recording_control::handle_touch(luma::touch_event const &event) -> std::bool_t {
for (auto const &point : event.points) {
phases.push_back(point.phase);
ids.push_back(point.id);
}
return true;
}
destroy_probe_control::destroy_probe_control(std::shared_ptr<lifetime_state> state)
: state_{std::move(state)} {}
destroy_probe_control::~destroy_probe_control() {
state_->destroyed = true;
if (state_->in_handle) {
state_->destroyed_while_active = true;
}
}
auto destroy_probe_control::measure(luma::draw_context &, luma::theme const &, luma::constraints) -> luma::size {
return {
.width = 20.0F,
.height = 20.0F,
};
}
auto destroy_probe_control::draw(luma::draw_context &, luma::theme const &) const -> void {}
closing_window_control::closing_window_control(luma::window_manager &manager, std::uint64_t &id, std::shared_ptr<lifetime_state> state)
: manager_{manager}, id_{id}, state_{std::move(state)} {}
closing_window_control::~closing_window_control() {
state_->destroyed = true;
if (state_->in_handle) {
state_->destroyed_while_active = true;
}
}
auto closing_window_control::measure(luma::draw_context &, luma::theme const &, luma::constraints) -> luma::size {
return {
.width = 20.0F,
.height = 20.0F,
};
}
auto closing_window_control::draw(luma::draw_context &, luma::theme const &) const -> void {}
auto closing_window_control::handle_touch(luma::touch_event const &) -> std::bool_t {
state_->in_handle = true;
auto status = manager_.close(id_);
state_->status_ok = status.has_value();
state_->in_handle = false;
return true;
}
opening_window_control::opening_window_control(luma::window_manager &manager, std::shared_ptr<lifetime_state> state)
: manager_{manager}, state_{std::move(state)} {}
auto opening_window_control::measure(luma::draw_context &, luma::theme const &, luma::constraints) -> luma::size {
return {
.width = 20.0F,
.height = 20.0F,
};
}
auto opening_window_control::draw(luma::draw_context &, luma::theme const &) const -> void {}
auto opening_window_control::handle_touch(luma::touch_event const &) -> std::bool_t {
auto opened = manager_.open({
.title = "Opened",
.bounds = {
.x = 250.0F,
.y = 100.0F,
.width = 50.0F,
.height = 50.0F,
},
},
std::make_unique<recording_control>());
state_->status_ok = opened.has_value();
if (opened) {
state_->opened_id = *opened;
state_->visible_during_callback = manager_.find(*opened) != nullptr;
}
return true;
}
updating_window_control::updating_window_control(luma::window_manager &manager, std::uint64_t &id, std::shared_ptr<lifetime_state> state)
: manager_{manager}, id_{id}, state_{std::move(state)} {}
auto updating_window_control::measure(luma::draw_context &, luma::theme const &, luma::constraints) -> luma::size {
return {
.width = 20.0F,
.height = 20.0F,
};
}
auto updating_window_control::draw(luma::draw_context &, luma::theme const &) const -> void {}
auto updating_window_control::handle_touch(luma::touch_event const &) -> std::bool_t {
auto bounds = manager_.set_bounds(id_, {
.x = 5.0F,
.y = 6.0F,
.width = 20.0F,
.height = 30.0F,
});
auto visible = manager_.set_visible(id_, false);
state_->status_ok = bounds.has_value() && visible.has_value();
if (auto *window = manager_.find(id_)) {
state_->visible_during_callback = window->visible();
}
return true;
}
closing_dialog_control::closing_dialog_control(luma::overlay_host &host, std::shared_ptr<lifetime_state> state, close_phase phase)
: host_{host}, state_{std::move(state)}, phase_{phase} {}
closing_dialog_control::~closing_dialog_control() {
state_->destroyed = true;
if (state_->in_handle) {
state_->destroyed_while_active = true;
}
}
auto closing_dialog_control::measure(luma::draw_context &, luma::theme const &, luma::constraints) -> luma::size {
close_if(close_phase::MEASURE);
return {
.width = 20.0F,
.height = 20.0F,
};
}
auto closing_dialog_control::arrange(luma::rect bounds) -> void {
luma::control::arrange(bounds);
close_if(close_phase::ARRANGE);
}
auto closing_dialog_control::draw(luma::draw_context &, luma::theme const &) const -> void {
close_if(close_phase::DRAW);
}
auto closing_dialog_control::handle_touch(luma::touch_event const &) -> std::bool_t {
close_if(close_phase::TOUCH);
return true;
}
auto closing_dialog_control::invalidate_layout() -> void {
close_if(close_phase::INVALIDATE);
}
auto closing_dialog_control::close_if(close_phase phase) const -> void {
if (phase_ != phase) {
return;
}
state_->in_handle = true;
auto status = host_.close_dialog(luma::dialog_result::CANCELED);
state_->status_ok = status.has_value();
state_->in_handle = false;
}
replacing_root_invalidate_control::replacing_root_invalidate_control(luma::application &app, std::shared_ptr<lifetime_state> state)
: app_{app}, state_{std::move(state)} {}
replacing_root_invalidate_control::~replacing_root_invalidate_control() {
state_->destroyed = true;
if (state_->in_handle) {
state_->destroyed_while_active = true;
}
}
auto replacing_root_invalidate_control::measure(luma::draw_context &, luma::theme const &, luma::constraints) -> luma::size {
return {
.width = 20.0F,
.height = 20.0F,
};
}
auto replacing_root_invalidate_control::draw(luma::draw_context &, luma::theme const &) const -> void {}
auto replacing_root_invalidate_control::invalidate_layout() -> void {
state_->in_handle = true;
auto next = std::unique_ptr<luma::control>{std::make_unique<recording_control>()};
auto status = app_.set_root(std::move(next));
state_->status_ok = status.has_value();
state_->in_handle = false;
}
closing_window_invalidate_control::closing_window_invalidate_control(luma::window_manager &manager, std::uint64_t &id, std::shared_ptr<lifetime_state> state)
: manager_{manager}, id_{id}, state_{std::move(state)} {}
closing_window_invalidate_control::~closing_window_invalidate_control() {
state_->destroyed = true;
if (state_->in_handle) {
state_->destroyed_while_active = true;
}
}
auto closing_window_invalidate_control::measure(luma::draw_context &, luma::theme const &, luma::constraints) -> luma::size {
return {
.width = 20.0F,
.height = 20.0F,
};
}
auto closing_window_invalidate_control::draw(luma::draw_context &, luma::theme const &) const -> void {}
auto closing_window_invalidate_control::invalidate_layout() -> void {
state_->in_handle = true;
auto status = manager_.close(id_);
state_->status_ok = status.has_value();
state_->in_handle = false;
}
}
BOOST_AUTO_TEST_CASE(debug_assert_mode_matches_build_type) {
#if defined(LUMA_EXPECT_NDEBUG) && !defined(NDEBUG)
BOOST_FAIL("release-style builds must define NDEBUG");
#endif
#if defined(LUMA_EXPECT_DEBUG_ASSERTS) && defined(NDEBUG)
BOOST_FAIL("debug builds must not define NDEBUG");
#endif
}
BOOST_AUTO_TEST_CASE(router_failure_uses_modulus_error) {
auto router = luma::router{};
auto result = router.replace("missing");
BOOST_REQUIRE(!result);
BOOST_TEST(std::string{result.error().category()} == "luma");
BOOST_TEST(result.error().code() == 2);
}
BOOST_AUTO_TEST_CASE(rect_contains_points) {
auto area = luma::rect{
.x = 10.0F,
.y = 20.0F,
.width = 100.0F,
.height = 50.0F,
};
BOOST_TEST(luma::contains(area, {
.x = 10.0F,
.y = 20.0F,
}));
BOOST_TEST(luma::contains(area, {
.x = 110.0F,
.y = 70.0F,
}));
BOOST_TEST(!luma::contains(area, {
.x = 111.0F,
.y = 70.0F,
}));
}
BOOST_AUTO_TEST_CASE(default_application_config_targets_scale_display) {
auto config = luma::application_config{};
BOOST_TEST(config.display.width == 1024);
BOOST_TEST(config.display.height == 600);
BOOST_TEST(config.touch.max_points == 5);
BOOST_TEST(config.display.idle_fps == 15);
}
BOOST_AUTO_TEST_CASE(router_replaces_current_view) {
auto router = luma::router{};
BOOST_REQUIRE(router.register_route("home", [] { return luma::label("Home"); }));
BOOST_REQUIRE(router.replace("home"));
BOOST_REQUIRE(router.current() != nullptr);
BOOST_TEST(router.current_route() == "home");
}
BOOST_AUTO_TEST_CASE(router_retire_handler_can_defer_replaced_views) {
auto router = luma::router{};
auto state = std::make_shared<lifetime_state>();
auto retired = std::vector<std::unique_ptr<luma::control>>{};
router.set_retire_handler([&retired](std::unique_ptr<luma::control> view) {
retired.push_back(std::move(view));
});
BOOST_REQUIRE(router.register_route("first", [state] {
return modulus::success(std::unique_ptr<luma::control>{std::make_unique<destroy_probe_control>(state)});
}));
BOOST_REQUIRE(router.register_route("second", [] {
return luma::label("Second");
}));
BOOST_REQUIRE(router.replace("first"));
BOOST_REQUIRE(router.replace("second"));
BOOST_TEST(!state->destroyed);
BOOST_TEST(retired.size() == 1U);
retired.clear();
BOOST_TEST(state->destroyed);
}
BOOST_AUTO_TEST_CASE(button_activates_on_release_inside_bounds) {
auto context = fake_draw_context{};
std::bool_t clicked = false;
auto control = luma::button("Tap", [&] { clicked = true; });
BOOST_REQUIRE(control);
(*control)->arrange({
.x = 0.0F,
.y = 0.0F,
.width = 100.0F,
.height = 50.0F,
});
(*control)->handle_touch({
.points = {
{
.id = 1,
.position = {
.x = 10.0F,
.y = 10.0F,
},
.phase = luma::touch_phase::DOWN,
},
},
});
(*control)->handle_touch({
.points = {
{
.id = 1,
.position = {
.x = 10.0F,
.y = 10.0F,
},
.phase = luma::touch_phase::UP,
},
},
});
BOOST_TEST(clicked);
}
BOOST_AUTO_TEST_CASE(button_uses_matching_touch_when_not_first_point) {
auto clicked = false;
auto control = luma::button("Tap", [&] {
clicked = true;
});
BOOST_REQUIRE(control);
(*control)->arrange({
.x = 0.0F,
.y = 0.0F,
.width = 100.0F,
.height = 50.0F,
});
(*control)->handle_touch({
.points = {
{
.id = 1,
.position = {
.x = 200.0F,
.y = 200.0F,
},
.phase = luma::touch_phase::DOWN,
},
{
.id = 2,
.position = {
.x = 10.0F,
.y = 10.0F,
},
.phase = luma::touch_phase::DOWN,
},
},
});
(*control)->handle_touch({
.points = {
{
.id = 1,
.position = {
.x = 200.0F,
.y = 200.0F,
},
.phase = luma::touch_phase::UP,
},
{
.id = 2,
.position = {
.x = 10.0F,
.y = 10.0F,
},
.phase = luma::touch_phase::UP,
},
},
});
BOOST_TEST(clicked);
}
BOOST_AUTO_TEST_CASE(label_measures_text) {
auto context = fake_draw_context{};
auto theme = luma::default_theme();
auto control = luma::label("Scale");
BOOST_REQUIRE(control);
auto measured = (*control)->measure(context, theme, {
.max = {
.width = 1024.0F,
.height = 600.0F,
},
});
BOOST_TEST(measured.width > 0.0F);
BOOST_TEST(measured.height > 0.0F);
}
BOOST_AUTO_TEST_CASE(text_controls_measure_with_supplied_theme) {
auto context = fake_draw_context{};
auto small = luma::default_theme();
small.body.size = 10.0F;
small.body.line_height = 12.0F;
auto large = small;
large.body.size = 40.0F;
large.body.line_height = 48.0F;
auto label = luma::label("Scale");
BOOST_REQUIRE(label);
auto small_label = (*label)->measure(context, small, {
.max = {
.width = 1024.0F,
.height = 600.0F,
},
});
auto large_label = (*label)->measure(context, large, {
.max = {
.width = 1024.0F,
.height = 600.0F,
},
});
BOOST_TEST(large_label.width > small_label.width);
BOOST_TEST(large_label.height > small_label.height);
auto button = luma::button("Scale", [] {});
BOOST_REQUIRE(button);
auto small_button = (*button)->measure(context, small, {
.max = {
.width = 1024.0F,
.height = 600.0F,
},
});
auto large_button = (*button)->measure(context, large, {
.max = {
.width = 1024.0F,
.height = 600.0F,
},
});
BOOST_TEST(large_button.width > small_button.width);
BOOST_TEST(large_button.height > small_button.height);
auto field = luma::text_field({
.text = "Scale",
},
[](std::string) {});
BOOST_REQUIRE(field);
auto small_field = (*field)->measure(context, small, {
.max = {
.width = 1024.0F,
.height = 600.0F,
},
});
auto large_field = (*field)->measure(context, large, {
.max = {
.width = 1024.0F,
.height = 600.0F,
},
});
BOOST_TEST(large_field.height > small_field.height);
}
BOOST_AUTO_TEST_CASE(text_controls_draw_with_supplied_theme_font) {
auto context = fake_draw_context{};
auto theme = luma::default_theme();
theme.body.font = "body.ttf";
auto control = luma::label("Scale");
BOOST_REQUIRE(control);
(*control)->arrange({
.x = 0.0F,
.y = 0.0F,
.width = 120.0F,
.height = 40.0F,
});
(*control)->draw(context, theme);
BOOST_REQUIRE(context.drawn_styles.size() == 1U);
BOOST_TEST(context.drawn_styles.front().font == "body.ttf");
}
BOOST_AUTO_TEST_CASE(application_dispatch_serializes_concurrent_callers) {
auto app = luma::application{};
auto values = std::vector<std::int32_t>{};
auto threads = std::vector<std::thread>{};
for (auto index = 0; index < 16; ++index) {
threads.emplace_back([&app, &values, index] {
auto result = app.dispatch([&values, index] -> std::expected<void, modulus::error> {
values.push_back(index);
return {};
});
BOOST_REQUIRE(result);
});
}
for (auto &thread : threads) {
thread.join();
}
BOOST_TEST(values.size() == 16U);
BOOST_TEST(std::accumulate(values.begin(), values.end(), 0) == 120);
}
BOOST_AUTO_TEST_CASE(application_routes_use_thread_safe_boundary) {
auto app = luma::application{};
BOOST_REQUIRE(app.register_route("home", [] { return luma::label("Home"); }));
BOOST_REQUIRE(app.replace_route("home"));
auto route = app.current_route();
BOOST_REQUIRE(route);
BOOST_TEST(*route == "home");
}
BOOST_AUTO_TEST_CASE(application_rejects_texture_load_without_renderer) {
auto app = luma::application{};
auto texture = app.load_texture("missing.png");
BOOST_REQUIRE(!texture);
BOOST_TEST(texture.error().code() == 3);
auto uncached = app.load_texture_uncached("missing.png");
BOOST_REQUIRE(!uncached);
BOOST_TEST(uncached.error().code() == 3);
auto empty = app.load_texture_uncached("");
BOOST_REQUIRE(!empty);
BOOST_TEST(empty.error().code() == 1);
}
BOOST_AUTO_TEST_CASE(application_show_toast_requires_root) {
auto app = luma::application{};
auto status = app.show_toast({
.message = "hello",
});
BOOST_REQUIRE(!status);
BOOST_TEST(status.error().code() == 3);
}
BOOST_AUTO_TEST_CASE(application_request_stop_is_thread_safe) {
auto app = luma::application{};
auto thread = std::thread{[&app] {
app.request_stop();
}};
thread.join();
BOOST_TEST(app.should_stop());
}
BOOST_AUTO_TEST_CASE(application_request_layout_is_thread_safe) {
auto app = luma::application{};
auto ok = false;
auto thread = std::thread{[&app, &ok] {
ok = app.request_layout().has_value();
}};
thread.join();
BOOST_TEST(ok);
}
BOOST_AUTO_TEST_CASE(application_request_layout_defers_root_replacement_during_invalidation) {
auto app = luma::application{};
auto state = std::make_shared<lifetime_state>();
auto root = std::unique_ptr<luma::control>{std::make_unique<replacing_root_invalidate_control>(app, state)};
BOOST_REQUIRE(app.set_root(std::move(root)));
auto status = app.request_layout();
BOOST_REQUIRE(status);
BOOST_TEST(state->status_ok);
BOOST_TEST(!state->destroyed_while_active);
BOOST_TEST(state->destroyed);
}
BOOST_AUTO_TEST_CASE(custom_control_unique_ptr_overloads_are_unambiguous) {
auto app = luma::application{};
BOOST_REQUIRE(app.set_root(std::make_unique<fixed_control>(luma::size{
.width = 10.0F,
.height = 10.0F,
})));
auto scroller = luma::scroll_view(std::make_unique<fixed_control>(luma::size{
.width = 10.0F,
.height = 100.0F,
}));
BOOST_REQUIRE(scroller);
auto children = luma::children(std::make_unique<fixed_control>(luma::size{
.width = 10.0F,
.height = 10.0F,
}),
luma::label("Label"));
BOOST_REQUIRE(children);
BOOST_TEST(children->size() == 2U);
}
BOOST_AUTO_TEST_CASE(factory_validation_returns_errors) {
BOOST_TEST(!luma::label(""));
BOOST_TEST(!luma::button("OK", {}));
BOOST_TEST(!luma::text_field({
.max_length = 0,
},
[](std::string) {}));
BOOST_TEST(!luma::image(nullptr));
BOOST_TEST(!luma::image(std::make_shared<fake_texture>(0, 16)));
BOOST_TEST(!luma::scroll_view(std::unique_ptr<luma::control>{}));
auto children = std::vector<std::unique_ptr<luma::control>>{};
children.push_back(nullptr);
BOOST_TEST(!luma::row(std::move(children)));
auto resources = fake_resources{};
auto assets = luma::asset_cache{resources};
auto texture = assets.texture("");
BOOST_REQUIRE(!texture);
BOOST_TEST(texture.error().code() == 1);
auto invalid_config = luma::application_config{};
invalid_config.theme.body.size = -1.0F;
auto config_status = luma::validate(invalid_config);
BOOST_REQUIRE(!config_status);
BOOST_TEST(config_status.error().code() == 1);
auto host = luma::overlays(nullptr);
BOOST_REQUIRE(!host);
BOOST_TEST(host.error().code() == 1);
auto manager = luma::window_manager{};
auto window = manager.open({}, nullptr);
BOOST_REQUIRE(!window);
BOOST_TEST(window.error().code() == 1);
}
BOOST_AUTO_TEST_CASE(asset_cache_allows_reentrant_resource_loads) {
auto resources = reentrant_resources{};
auto assets = luma::asset_cache{resources};
resources.cache = &assets;
auto texture = assets.texture("outer.png");
BOOST_REQUIRE(texture);
BOOST_TEST(resources.inner_loaded);
BOOST_TEST(resources.load_count == 2);
BOOST_REQUIRE(assets.texture("outer.png"));
BOOST_TEST(resources.load_count == 2);
}
BOOST_AUTO_TEST_CASE(asset_cache_serializes_concurrent_same_path_loads) {
auto resources = blocking_resources{};
auto assets = luma::asset_cache{resources};
auto start_mutex = std::mutex{};
auto start_ready = std::condition_variable{};
auto start = false;
auto ok = std::vector<std::int32_t>(4, 0);
auto textures = std::vector<std::shared_ptr<luma::texture>>(4);
auto threads = std::vector<std::thread>{};
for (auto index = std::size_t{}; index < ok.size(); ++index) {
threads.emplace_back([&assets, &ok, &textures, &start_mutex, &start_ready, &start, index] {
{
auto lock = std::unique_lock{start_mutex};
start_ready.wait(lock, [&start] {
return start;
});
}
auto texture = assets.texture("shared.png");
ok[index] = texture.has_value() ? 1 : 0;
if (texture) {
textures[index] = *texture;
}
});
}
{
auto lock = std::scoped_lock{start_mutex};
start = true;
}
start_ready.notify_all();
BOOST_REQUIRE(resources.wait_for_loads(1));
resources.release();
for (auto &thread : threads) {
thread.join();
}
BOOST_TEST(resources.load_count() == 1);
for (auto value : ok) {
BOOST_TEST(value == 1);
}
for (auto const &texture : textures) {
BOOST_TEST(texture == textures.front());
}
}
BOOST_AUTO_TEST_CASE(asset_cache_rejects_recursive_same_path_load) {
auto resources = same_path_reentrant_resources{};
auto assets = luma::asset_cache{resources};
resources.cache = &assets;
auto texture = assets.texture("same.png");
BOOST_REQUIRE(texture);
BOOST_TEST(resources.load_count == 1);
BOOST_TEST(resources.recursive_failed);
BOOST_TEST(resources.recursive_code == 3);
}
BOOST_AUTO_TEST_CASE(asset_cache_clear_does_not_retain_an_in_flight_texture) {
auto resources = blocking_resources{};
auto assets = luma::asset_cache{resources};
auto first = std::expected<std::shared_ptr<luma::texture>, modulus::error>{};
auto thread = std::thread{[&] { first = assets.texture("page.png"); }};
auto entered = resources.wait_for_loads(1);
assets.clear();
resources.release();
thread.join();
BOOST_REQUIRE(entered);
BOOST_REQUIRE(first);
auto second = assets.texture("page.png");
BOOST_REQUIRE(second);
BOOST_TEST(resources.load_count() == 2);
BOOST_TEST(*first != *second);
BOOST_REQUIRE(assets.texture("page.png"));
BOOST_TEST(resources.load_count() == 2);
}
BOOST_AUTO_TEST_CASE(asset_cache_failed_loads_complete_and_can_be_retried) {
struct resources final : luma::renderer_resources {
std::uint32_t calls{};
auto load_texture(std::string const &path) -> std::expected<std::shared_ptr<luma::texture>, modulus::error> override {
if (++calls == 1U) {
if (path == "null") {
return std::shared_ptr<luma::texture>{};
}
if (path == "throw") {
throw std::runtime_error{"loader failed"};
}
return std::unexpected(modulus::make_error("loader", 5, "failed"));
}
return std::make_shared<fake_texture>();
}
};
for (auto const *path : {"null", "throw", "error"}) {
auto loader = resources{};
auto assets = luma::asset_cache{loader};
BOOST_CHECK(!assets.texture(path));
BOOST_REQUIRE(assets.texture(path));
BOOST_REQUIRE(assets.texture(path));
BOOST_TEST(loader.calls == 2U);
}
}
BOOST_AUTO_TEST_CASE(theme_numbers_reject_nonfinite_and_unrepresentable_values) {
for (auto number : {-1.0, std::numeric_limits<double>::infinity(), std::numeric_limits<double>::quiet_NaN(), std::numeric_limits<double>::max()}) {
auto parsed = luma::parse_theme(boost::json::object{{"spacing", number}});
BOOST_REQUIRE(!parsed);
BOOST_TEST(std::string{parsed.error().category()} == "luma");
}
auto parsed = luma::parse_theme(boost::json::object{{"spacing", std::uint64_t{8}}});
BOOST_REQUIRE(parsed);
BOOST_TEST(parsed->spacing == 8.0F);
}
BOOST_AUTO_TEST_CASE(text_input_state_filters_modes_and_keyboard_updates_bound_field) {
auto changes = std::vector<std::string>{};
auto input = luma::make_text_input({
.mode = luma::text_input_mode::INTEGER,
.max_length = 3,
},
[&](std::string text) {
changes.push_back(std::move(text));
});
BOOST_REQUIRE(input);
BOOST_REQUIRE((*input)->append("1"));
BOOST_TEST(!(*input)->append("x"));
BOOST_REQUIRE((*input)->append("2"));
BOOST_TEST(!(*input)->append("345"));
BOOST_TEST((*input)->text() == "12");
auto keyboard = luma::soft_keyboard({
.rows = {
{
{
"3",
"3",
},
{
"<-",
"",
luma::keyboard_key_kind::BACKSPACE,
},
},
},
},
*input);
BOOST_REQUIRE(keyboard);
(*keyboard)->arrange({
.x = 0.0F,
.y = 0.0F,
.width = 200.0F,
.height = 50.0F,
});
(*keyboard)->handle_touch({
.points = {
{
.id = 1,
.position = {
.x = 25.0F,
.y = 25.0F,
},
.phase = luma::touch_phase::UP,
},
},
});
BOOST_TEST((*input)->text() == "123");
(*keyboard)->handle_touch({
.points = {
{
.id = 1,
.position = {
.x = 150.0F,
.y = 25.0F,
},
.phase = luma::touch_phase::UP,
},
},
});
BOOST_TEST((*input)->text() == "12");
BOOST_TEST(!changes.empty());
}
BOOST_AUTO_TEST_CASE(decimal_text_input_accepts_one_decimal_point) {
auto input = luma::make_text_input({
.mode = luma::text_input_mode::DECIMAL,
},
[](std::string) {});
BOOST_REQUIRE(input);
BOOST_REQUIRE((*input)->append("1.5"));
BOOST_TEST(!(*input)->append("."));
BOOST_TEST(!(*input)->append("a"));
BOOST_TEST((*input)->text() == "1.5");
}
BOOST_AUTO_TEST_CASE(text_input_callback_exceptions_return_errors) {
auto input = luma::make_text_input({}, [](std::string) {
throw std::runtime_error{"boom"};
});
BOOST_REQUIRE(input);
auto status = (*input)->append("x");
BOOST_REQUIRE(!status);
BOOST_TEST(status.error().code() == 5);
}
BOOST_AUTO_TEST_CASE(text_input_state_notifies_bound_layout_invalidator) {
auto input = luma::make_text_input({
.text = "a",
},
[](std::string) {});
BOOST_REQUIRE(input);
auto field = luma::text_field(*input);
BOOST_REQUIRE(field);
auto invalidations = 0;
(*field)->bind_layout_invalidator([&] {
++invalidations;
});
BOOST_REQUIRE((*input)->set_text("longer"));
BOOST_TEST(invalidations == 1);
BOOST_REQUIRE((*input)->append("x"));
BOOST_TEST(invalidations == 2);
BOOST_REQUIRE((*input)->backspace());
BOOST_TEST(invalidations == 3);
field->reset();
BOOST_REQUIRE((*input)->set_text("after"));
BOOST_TEST(invalidations == 3);
}
BOOST_AUTO_TEST_CASE(text_field_layout_invalidator_propagates_through_containers) {
auto input = luma::make_text_input({
.text = "a",
},
[](std::string) {});
BOOST_REQUIRE(input);
auto field = luma::text_field(*input);
BOOST_REQUIRE(field);
auto children = luma::children(std::move(*field));
BOOST_REQUIRE(children);
auto column = luma::column(std::move(*children));
BOOST_REQUIRE(column);
auto invalidations = 0;
(*column)->bind_layout_invalidator([&] {
++invalidations;
});
BOOST_REQUIRE((*input)->set_text("longer"));
BOOST_TEST(invalidations == 1);
}
BOOST_AUTO_TEST_CASE(dialog_accept_and_cancel_close_with_callbacks) {
auto root = luma::label("Root");
BOOST_REQUIRE(root);
auto host_result = luma::overlays(std::move(*root));
BOOST_REQUIRE(host_result);
auto &host = **host_result;
host.arrange({
.x = 0.0F,
.y = 0.0F,
.width = 1000.0F,
.height = 600.0F,
});
auto result = luma::dialog_result::NONE;
auto body = luma::label("Body");
BOOST_REQUIRE(body);
BOOST_REQUIRE(host.show_dialog({
.title = "Title",
.body = std::move(*body),
.on_close = [&](luma::dialog_result value) { result = value; },
}));
host.handle_touch({
.points = {
{
.id = 1,
.position = {
.x = 800.0F,
.y = 470.0F,
},
.phase = luma::touch_phase::UP,
},
},
});
BOOST_TEST(static_cast<std::int32_t>(result) == static_cast<std::int32_t>(luma::dialog_result::ACCEPTED));
result = luma::dialog_result::NONE;
body = luma::label("Body");
BOOST_REQUIRE(body);
BOOST_REQUIRE(host.show_dialog({
.title = "Title",
.body = std::move(*body),
.on_close = [&](luma::dialog_result value) { result = value; },
}));
host.handle_touch({
.points = {
{
.id = 1,
.position = {
.x = 660.0F,
.y = 470.0F,
},
.phase = luma::touch_phase::UP,
},
},
});
BOOST_TEST(static_cast<std::int32_t>(result) == static_cast<std::int32_t>(luma::dialog_result::CANCELED));
}
BOOST_AUTO_TEST_CASE(dialog_uses_matching_touch_when_not_first_point) {
auto root = luma::label("Root");
BOOST_REQUIRE(root);
auto host_result = luma::overlays(std::move(*root));
BOOST_REQUIRE(host_result);
auto &host = **host_result;
host.arrange({
.x = 0.0F,
.y = 0.0F,
.width = 1000.0F,
.height = 600.0F,
});
auto result = luma::dialog_result::NONE;
auto body = luma::label("Body");
BOOST_REQUIRE(body);
BOOST_REQUIRE(host.show_dialog({
.title = "Title",
.body = std::move(*body),
.on_close = [&](luma::dialog_result value) { result = value; },
}));
host.handle_touch({
.points = {
{
.id = 1,
.position = {
.x = 20.0F,
.y = 20.0F,
},
.phase = luma::touch_phase::UP,
},
{
.id = 2,
.position = {
.x = 800.0F,
.y = 470.0F,
},
.phase = luma::touch_phase::UP,
},
},
});
BOOST_TEST(static_cast<std::int32_t>(result) == static_cast<std::int32_t>(luma::dialog_result::ACCEPTED));
}
BOOST_AUTO_TEST_CASE(dialog_body_receives_touches) {
auto root = luma::label("Root");
BOOST_REQUIRE(root);
auto host_result = luma::overlays(std::move(*root));
BOOST_REQUIRE(host_result);
auto &host = **host_result;
auto body = std::make_unique<fixed_control>(luma::size{
.width = 100.0F,
.height = 100.0F,
});
auto *body_ptr = body.get();
BOOST_REQUIRE(host.show_dialog({
.title = "Title",
.body = std::move(body),
}));
host.arrange({
.x = 0.0F,
.y = 0.0F,
.width = 1000.0F,
.height = 600.0F,
});
host.handle_touch({
.points = {
{
.id = 1,
.position = {
.x = 120.0F,
.y = 150.0F,
},
.phase = luma::touch_phase::DOWN,
},
},
});
BOOST_TEST(body_ptr->touched);
}
BOOST_AUTO_TEST_CASE(dialog_body_can_close_dialog_from_forwarded_methods_without_mid_callback_destruction) {
auto context = fake_draw_context{};
auto theme = luma::default_theme();
auto phases = std::vector<close_phase>{
close_phase::MEASURE,
close_phase::ARRANGE,
close_phase::DRAW,
close_phase::TOUCH,
close_phase::INVALIDATE,
};
for (auto phase : phases) {
auto root = luma::label("Root");
BOOST_REQUIRE(root);
auto host_result = luma::overlays(std::move(*root));
BOOST_REQUIRE(host_result);
auto &host = **host_result;
auto state = std::make_shared<lifetime_state>();
auto body = std::make_unique<closing_dialog_control>(host, state, phase);
BOOST_REQUIRE(host.show_dialog({
.title = "Title",
.body = std::move(body),
}));
if (phase == close_phase::MEASURE) {
host.measure(context, theme, {
.max = {
.width = 1000.0F,
.height = 600.0F,
},
});
} else if (phase == close_phase::ARRANGE) {
host.arrange({
.x = 0.0F,
.y = 0.0F,
.width = 1000.0F,
.height = 600.0F,
});
} else if (phase == close_phase::DRAW) {
host.arrange({
.x = 0.0F,
.y = 0.0F,
.width = 1000.0F,
.height = 600.0F,
});
host.draw(context, theme);
} else if (phase == close_phase::TOUCH) {
host.arrange({
.x = 0.0F,
.y = 0.0F,
.width = 1000.0F,
.height = 600.0F,
});
host.handle_touch({
.points = {
{
.id = 1,
.position = {
.x = 120.0F,
.y = 150.0F,
},
.phase = luma::touch_phase::DOWN,
},
},
});
} else {
host.invalidate_layout();
}
BOOST_TEST(state->status_ok);
BOOST_TEST(!state->destroyed_while_active);
BOOST_TEST(state->destroyed);
}
}
BOOST_AUTO_TEST_CASE(window_manager_captures_touch_by_id) {
auto manager = luma::window_manager{};
auto content = std::make_unique<recording_control>();
auto *content_ptr = content.get();
auto id = manager.open({
.title = "Tool",
.bounds = {
.x = 100.0F,
.y = 100.0F,
.width = 100.0F,
.height = 100.0F,
},
},
std::move(content));
BOOST_REQUIRE(id);
BOOST_TEST(*id != 0U);
BOOST_TEST(manager.handle_touch({
.points = {
{
.id = 1,
.position = {
.x = 10.0F,
.y = 10.0F,
},
.phase = luma::touch_phase::DOWN,
},
{
.id = 2,
.position = {
.x = 120.0F,
.y = 150.0F,
},
.phase = luma::touch_phase::DOWN,
},
},
}));
BOOST_REQUIRE(content_ptr->ids.size() == 1U);
BOOST_TEST(content_ptr->ids.front() == 2);
BOOST_TEST(static_cast<std::int32_t>(content_ptr->phases.front()) == static_cast<std::int32_t>(luma::touch_phase::DOWN));
BOOST_TEST(manager.handle_touch({
.points = {
{
.id = 2,
.position = {
.x = 20.0F,
.y = 20.0F,
},
.phase = luma::touch_phase::UP,
},
},
}));
BOOST_REQUIRE(content_ptr->phases.size() == 2U);
BOOST_TEST(static_cast<std::int32_t>(content_ptr->phases.back()) == static_cast<std::int32_t>(luma::touch_phase::UP));
}
BOOST_AUTO_TEST_CASE(window_set_bounds_validates_release_inputs) {
auto manager = luma::window_manager{};
auto id = manager.open({
.title = "Tool",
.bounds = {
.x = 100.0F,
.y = 100.0F,
.width = 100.0F,
.height = 100.0F,
},
},
std::make_unique<recording_control>());
BOOST_REQUIRE(id);
auto *window = manager.find(*id);
BOOST_REQUIRE(window != nullptr);
auto original = window->options().bounds;
auto invalid = window->set_bounds({
.x = 0.0F,
.y = 0.0F,
.width = -1.0F,
.height = 10.0F,
});
BOOST_REQUIRE(!invalid);
BOOST_TEST(window->options().bounds.width == original.width);
auto valid = window->set_bounds({
.x = 5.0F,
.y = 6.0F,
.width = 20.0F,
.height = 30.0F,
});
BOOST_REQUIRE(valid);
BOOST_TEST(window->options().bounds.x == 5.0F);
BOOST_TEST(window->options().bounds.height == 30.0F);
}
BOOST_AUTO_TEST_CASE(window_manager_uses_decorated_client_area_for_content) {
auto context = fake_draw_context{};
auto theme = luma::default_theme();
auto manager = luma::window_manager{};
auto content = std::make_unique<filling_control>();
auto *content_ptr = content.get();
auto id = manager.open({
.title = "Tool",
.bounds = {
.x = 10.0F,
.y = 20.0F,
.width = 100.0F,
.height = 80.0F,
},
},
std::move(content));
BOOST_REQUIRE(id);
manager.measure(context, theme, {
.max = {
.width = 200.0F,
.height = 200.0F,
},
});
manager.arrange({
.x = 0.0F,
.y = 0.0F,
.width = 200.0F,
.height = 200.0F,
});
BOOST_TEST(content_ptr->arranged.x == 10.0F);
BOOST_TEST(content_ptr->arranged.y == 62.0F);
BOOST_TEST(content_ptr->arranged.width == 100.0F);
BOOST_TEST(content_ptr->arranged.height == 38.0F);
manager.draw(context, theme);
BOOST_REQUIRE(context.clips.size() == 1U);
BOOST_TEST(context.clips.front().y == 62.0F);
BOOST_TEST(context.clips.front().height == 38.0F);
BOOST_REQUIRE(context.filled.size() >= 3U);
BOOST_TEST(context.filled[0].y == 20.0F);
BOOST_TEST(context.filled[1].y == 62.0F);
BOOST_TEST(context.filled[2].y == 20.0F);
BOOST_TEST(context.clip_depth == 0);
}
BOOST_AUTO_TEST_CASE(window_manager_uses_theme_body_line_height_for_decorated_chrome) {
auto context = fake_draw_context{};
auto theme = luma::default_theme();
theme.body.line_height = 60.0F;
auto manager = luma::window_manager{};
auto content = std::make_unique<filling_control>();
auto *content_ptr = content.get();
auto id = manager.open({
.title = "Tool",
.bounds = {
.x = 10.0F,
.y = 20.0F,
.width = 100.0F,
.height = 100.0F,
},
},
std::move(content));
BOOST_REQUIRE(id);
manager.measure(context, theme, {
.max = {
.width = 200.0F,
.height = 200.0F,
},
});
manager.arrange({
.x = 0.0F,
.y = 0.0F,
.width = 200.0F,
.height = 200.0F,
});
BOOST_TEST(content_ptr->arranged.y == 92.0F);
BOOST_TEST(content_ptr->arranged.height == 28.0F);
manager.draw(context, theme);
BOOST_REQUIRE(context.clips.size() == 1U);
BOOST_TEST(context.clips.front().y == 92.0F);
BOOST_TEST(context.clips.front().height == 28.0F);
BOOST_REQUIRE(!context.drawn_origins.empty());
BOOST_TEST(context.drawn_origins.back().y == 26.0F);
}
BOOST_AUTO_TEST_CASE(window_manager_skips_hidden_windows_during_measure) {
auto context = fake_draw_context{};
auto theme = luma::default_theme();
auto manager = luma::window_manager{};
auto content = std::make_unique<counting_control>(luma::size{
.width = 20.0F,
.height = 20.0F,
});
auto *content_ptr = content.get();
auto id = manager.open({
.title = "Tool",
.bounds = {
.x = 10.0F,
.y = 20.0F,
.width = 100.0F,
.height = 80.0F,
},
.visible = false,
},
std::move(content));
BOOST_REQUIRE(id);
manager.measure(context, theme, {
.max = {
.width = 200.0F,
.height = 200.0F,
},
});
BOOST_TEST(content_ptr->measure_count == 0);
BOOST_REQUIRE(manager.set_visible(*id, true));
manager.measure(context, theme, {
.max = {
.width = 200.0F,
.height = 200.0F,
},
});
BOOST_TEST(content_ptr->measure_count == 1);
}
BOOST_AUTO_TEST_CASE(window_manager_id_mutators_validate_and_update_windows) {
auto manager = luma::window_manager{};
auto id = manager.open({
.title = "Tool",
.bounds = {
.x = 100.0F,
.y = 100.0F,
.width = 100.0F,
.height = 100.0F,
},
},
std::make_unique<recording_control>());
BOOST_REQUIRE(id);
BOOST_TEST(!manager.set_bounds(0, {
.x = 1.0F,
.y = 2.0F,
.width = 3.0F,
.height = 4.0F,
}));
BOOST_TEST(!manager.set_bounds(*id, {
.x = 1.0F,
.y = 2.0F,
.width = -3.0F,
.height = 4.0F,
}));
BOOST_REQUIRE(manager.set_bounds(*id, {
.x = 5.0F,
.y = 6.0F,
.width = 20.0F,
.height = 30.0F,
}));
BOOST_REQUIRE(manager.set_visible(*id, false));
auto *window = manager.find(*id);
BOOST_REQUIRE(window != nullptr);
BOOST_TEST(window->options().bounds.x == 5.0F);
BOOST_TEST(window->options().bounds.height == 30.0F);
BOOST_TEST(!window->visible());
}
BOOST_AUTO_TEST_CASE(window_manager_find_pointer_is_stable_across_open_and_reorder) {
auto manager = luma::window_manager{};
auto first = manager.open({
.title = "First",
.bounds = {
.x = 0.0F,
.y = 0.0F,
.width = 10.0F,
.height = 10.0F,
},
},
std::make_unique<recording_control>());
BOOST_REQUIRE(first);
auto *first_ptr = manager.find(*first);
BOOST_REQUIRE(first_ptr != nullptr);
for (auto index = 0; index < 32; ++index) {
auto opened = manager.open({
.title = "Other",
.bounds = {
.x = static_cast<std::float32_t>(index + 20),
.y = 0.0F,
.width = 10.0F,
.height = 10.0F,
},
},
std::make_unique<recording_control>());
BOOST_REQUIRE(opened);
}
BOOST_TEST(manager.find(*first) == first_ptr);
BOOST_REQUIRE(manager.bring_to_front(*first));
BOOST_TEST(manager.find(*first) == first_ptr);
}
BOOST_AUTO_TEST_CASE(window_manager_defers_reentrant_close_until_touch_returns) {
auto manager = luma::window_manager{};
auto state = std::make_shared<lifetime_state>();
auto id = std::uint64_t{};
auto opened = manager.open({
.title = "Tool",
.bounds = {
.x = 100.0F,
.y = 100.0F,
.width = 100.0F,
.height = 100.0F,
},
},
std::make_unique<closing_window_control>(manager, id, state));
BOOST_REQUIRE(opened);
id = *opened;
BOOST_TEST(manager.handle_touch({
.points = {
{
.id = 1,
.position = {
.x = 120.0F,
.y = 150.0F,
},
.phase = luma::touch_phase::DOWN,
},
},
}));
BOOST_TEST(state->status_ok);
BOOST_TEST(!state->destroyed_while_active);
BOOST_TEST(state->destroyed);
BOOST_TEST(manager.find(id) == nullptr);
}
BOOST_AUTO_TEST_CASE(window_manager_defers_reentrant_close_until_invalidation_returns) {
auto manager = luma::window_manager{};
auto state = std::make_shared<lifetime_state>();
auto id = std::uint64_t{};
auto opened = manager.open({
.title = "Tool",
.bounds = {
.x = 100.0F,
.y = 100.0F,
.width = 100.0F,
.height = 100.0F,
},
},
std::make_unique<closing_window_invalidate_control>(manager, id, state));
BOOST_REQUIRE(opened);
id = *opened;
manager.invalidate_layout();
BOOST_TEST(state->status_ok);
BOOST_TEST(!state->destroyed_while_active);
BOOST_TEST(state->destroyed);
BOOST_TEST(manager.find(id) == nullptr);
}
BOOST_AUTO_TEST_CASE(window_manager_defers_reentrant_open_and_returns_reserved_id) {
auto manager = luma::window_manager{};
auto state = std::make_shared<lifetime_state>();
auto opened = manager.open({
.title = "Tool",
.bounds = {
.x = 100.0F,
.y = 100.0F,
.width = 100.0F,
.height = 100.0F,
},
},
std::make_unique<opening_window_control>(manager, state));
BOOST_REQUIRE(opened);
BOOST_TEST(manager.handle_touch({
.points = {
{
.id = 1,
.position = {
.x = 120.0F,
.y = 150.0F,
},
.phase = luma::touch_phase::DOWN,
},
},
}));
BOOST_TEST(state->status_ok);
BOOST_TEST(state->opened_id != 0U);
BOOST_TEST(!state->visible_during_callback);
BOOST_TEST(manager.find(state->opened_id) != nullptr);
}
BOOST_AUTO_TEST_CASE(window_manager_defers_reentrant_bounds_and_visibility_updates) {
auto manager = luma::window_manager{};
auto state = std::make_shared<lifetime_state>();
auto id = std::uint64_t{};
auto opened = manager.open({
.title = "Tool",
.bounds = {
.x = 100.0F,
.y = 100.0F,
.width = 100.0F,
.height = 100.0F,
},
},
std::make_unique<updating_window_control>(manager, id, state));
BOOST_REQUIRE(opened);
id = *opened;
BOOST_TEST(manager.handle_touch({
.points = {
{
.id = 1,
.position = {
.x = 120.0F,
.y = 150.0F,
},
.phase = luma::touch_phase::DOWN,
},
},
}));
BOOST_TEST(state->status_ok);
BOOST_TEST(state->visible_during_callback);
auto *window = manager.find(id);
BOOST_REQUIRE(window != nullptr);
BOOST_TEST(window->options().bounds.x == 5.0F);
BOOST_TEST(window->options().bounds.height == 30.0F);
BOOST_TEST(!window->visible());
}
BOOST_AUTO_TEST_CASE(application_window_facade_validates_pre_run_calls) {
auto app = luma::application{};
auto opened = app.open_window({
.title = "Tool",
.bounds = {
.x = 100.0F,
.y = 100.0F,
.width = 100.0F,
.height = 100.0F,
},
},
std::make_unique<recording_control>());
BOOST_REQUIRE(opened);
BOOST_REQUIRE(app.set_window_bounds(*opened, {
.x = 5.0F,
.y = 6.0F,
.width = 20.0F,
.height = 30.0F,
}));
BOOST_REQUIRE(app.set_window_visible(*opened, false));
BOOST_TEST(!app.set_window_bounds(0, {
.x = 5.0F,
.y = 6.0F,
.width = 20.0F,
.height = 30.0F,
}));
BOOST_TEST(!app.set_window_bounds(*opened, {
.x = 5.0F,
.y = 6.0F,
.width = -20.0F,
.height = 30.0F,
}));
}
BOOST_AUTO_TEST_CASE(containers_route_simultaneous_touches_to_siblings) {
auto context = fake_draw_context{};
auto theme = luma::default_theme();
auto first = std::make_unique<fixed_control>(luma::size{
.width = 50.0F,
.height = 50.0F,
});
auto *first_ptr = first.get();
auto second = std::make_unique<fixed_control>(luma::size{
.width = 50.0F,
.height = 50.0F,
});
auto *second_ptr = second.get();
auto control = luma::row(luma::children(modulus::success(std::unique_ptr<luma::control>{std::move(first)}), modulus::success(std::unique_ptr<luma::control>{std::move(second)})), 0.0F, luma::alignment::START);
BOOST_REQUIRE(control);
(*control)->measure(context, theme, {
.max = {
.width = 100.0F,
.height = 50.0F,
},
});
(*control)->arrange({
.x = 0.0F,
.y = 0.0F,
.width = 100.0F,
.height = 50.0F,
});
BOOST_TEST((*control)->handle_touch({
.points = {
{
.id = 1,
.position = {
.x = 10.0F,
.y = 10.0F,
},
.phase = luma::touch_phase::DOWN,
},
{
.id = 2,
.position = {
.x = 60.0F,
.y = 10.0F,
},
.phase = luma::touch_phase::DOWN,
},
},
}));
BOOST_TEST(first_ptr->touched);
BOOST_TEST(second_ptr->touched);
}
BOOST_AUTO_TEST_CASE(row_and_column_apply_cross_axis_alignment) {
auto context = fake_draw_context{};
auto theme = luma::default_theme();
auto first = std::make_unique<fixed_control>(luma::size{
.width = 20.0F,
.height = 10.0F,
});
auto *first_ptr = first.get();
auto second = std::make_unique<fixed_control>(luma::size{
.width = 20.0F,
.height = 20.0F,
});
auto *second_ptr = second.get();
auto row = luma::row(luma::children(modulus::success(std::unique_ptr<luma::control>{std::move(first)}), modulus::success(std::unique_ptr<luma::control>{std::move(second)})), 0.0F, luma::alignment::END);
BOOST_REQUIRE(row);
(*row)->measure(context, theme, {
.max = {
.width = 100.0F,
.height = 100.0F,
},
});
(*row)->arrange({
.x = 0.0F,
.y = 0.0F,
.width = 100.0F,
.height = 100.0F,
});
BOOST_TEST(first_ptr->arranged.y == 90.0F);
BOOST_TEST(second_ptr->arranged.y == 80.0F);
auto child = std::make_unique<fixed_control>(luma::size{
.width = 20.0F,
.height = 10.0F,
});
auto *child_ptr = child.get();
auto column = luma::column(luma::children(modulus::success(std::unique_ptr<luma::control>{std::move(child)})), 0.0F, luma::alignment::CENTER);
BOOST_REQUIRE(column);
(*column)->measure(context, theme, {
.max = {
.width = 100.0F,
.height = 100.0F,
},
});
(*column)->arrange({
.x = 0.0F,
.y = 0.0F,
.width = 100.0F,
.height = 100.0F,
});
BOOST_TEST(child_ptr->arranged.x == 40.0F);
}
BOOST_AUTO_TEST_CASE(panel_measure_includes_padding) {
auto context = fake_draw_context{};
auto theme = luma::default_theme();
auto child = std::make_unique<fixed_control>(luma::size{
.width = 20.0F,
.height = 10.0F,
});
auto control = luma::panel(luma::children(modulus::success(std::unique_ptr<luma::control>{std::move(child)})));
BOOST_REQUIRE(control);
auto measured = (*control)->measure(context, theme, {
.max = {
.width = 100.0F,
.height = 100.0F,
},
});
BOOST_TEST(measured.width == 36.0F);
BOOST_TEST(measured.height == 26.0F);
}
BOOST_AUTO_TEST_CASE(scroll_view_uses_nested_clips) {
auto context = fake_draw_context{};
auto theme = luma::default_theme();
auto inner = luma::scroll_view(luma::label("Inner"));
auto outer = luma::scroll_view(std::move(inner));
BOOST_REQUIRE(outer);
(*outer)->measure(context, theme, {
.max = {
.width = 100.0F,
.height = 100.0F,
},
});
(*outer)->arrange({
.x = 0.0F,
.y = 0.0F,
.width = 100.0F,
.height = 100.0F,
});
(*outer)->draw(context, theme);
BOOST_TEST(context.max_clip_depth == 2);
BOOST_TEST(context.clip_depth == 0);
}
BOOST_AUTO_TEST_CASE(scroll_view_forwards_taps_to_child) {
auto context = fake_draw_context{};
auto theme = luma::default_theme();
auto clicked = false;
auto control = luma::scroll_view(luma::button("Tap", [&] {
clicked = true;
}));
BOOST_REQUIRE(control);
(*control)->measure(context, theme, {
.max = {
.width = 100.0F,
.height = 100.0F,
},
});
(*control)->arrange({
.x = 0.0F,
.y = 0.0F,
.width = 100.0F,
.height = 100.0F,
});
(*control)->handle_touch({
.points = {
{
.id = 1,
.position = {
.x = 20.0F,
.y = 20.0F,
},
.phase = luma::touch_phase::DOWN,
},
},
});
(*control)->handle_touch({
.points = {
{
.id = 1,
.position = {
.x = 20.0F,
.y = 20.0F,
},
.phase = luma::touch_phase::UP,
},
},
});
BOOST_TEST(clicked);
}
BOOST_AUTO_TEST_CASE(scroll_view_forwards_simultaneous_child_taps) {
auto context = fake_draw_context{};
auto theme = luma::default_theme();
auto first_clicked = false;
auto second_clicked = false;
auto content = luma::row(luma::children(luma::button("A", [&] { first_clicked = true; }), luma::button("B", [&] { second_clicked = true; })), 0.0F, luma::alignment::START);
auto control = luma::scroll_view(std::move(content));
BOOST_REQUIRE(control);
(*control)->measure(context, theme, {
.max = {
.width = 100.0F,
.height = 100.0F,
},
});
(*control)->arrange({
.x = 0.0F,
.y = 0.0F,
.width = 100.0F,
.height = 100.0F,
});
(*control)->handle_touch({
.points = {
{
.id = 1,
.position = {
.x = 20.0F,
.y = 20.0F,
},
.phase = luma::touch_phase::DOWN,
},
{
.id = 2,
.position = {
.x = 70.0F,
.y = 20.0F,
},
.phase = luma::touch_phase::DOWN,
},
},
});
(*control)->handle_touch({
.points = {
{
.id = 1,
.position = {
.x = 20.0F,
.y = 20.0F,
},
.phase = luma::touch_phase::UP,
},
{
.id = 2,
.position = {
.x = 70.0F,
.y = 20.0F,
},
.phase = luma::touch_phase::UP,
},
},
});
BOOST_TEST(first_clicked);
BOOST_TEST(second_clicked);
}
BOOST_AUTO_TEST_CASE(scroll_view_drag_suppresses_child_click) {
auto context = fake_draw_context{};
auto theme = luma::default_theme();
auto clicked = false;
auto control = luma::scroll_view(luma::button("Tap", [&] {
clicked = true;
}));
BOOST_REQUIRE(control);
(*control)->measure(context, theme, {
.max = {
.width = 100.0F,
.height = 100.0F,
},
});
(*control)->arrange({
.x = 0.0F,
.y = 0.0F,
.width = 100.0F,
.height = 100.0F,
});
(*control)->handle_touch({
.points = {
{
.id = 1,
.position = {
.x = 20.0F,
.y = 80.0F,
},
.phase = luma::touch_phase::DOWN,
},
},
});
(*control)->handle_touch({
.points = {
{
.id = 1,
.position = {
.x = 20.0F,
.y = 20.0F,
},
.phase = luma::touch_phase::MOVE,
},
},
});
(*control)->handle_touch({
.points = {
{
.id = 1,
.position = {
.x = 20.0F,
.y = 20.0F,
},
.phase = luma::touch_phase::UP,
},
},
});
BOOST_TEST(!clicked);
}
BOOST_AUTO_TEST_CASE(scroll_view_allows_content_beyond_four_viewports) {
auto context = fake_draw_context{};
auto theme = luma::default_theme();
auto child = std::make_unique<fixed_control>(luma::size{
.width = 20.0F,
.height = 1000.0F,
});
auto *child_ptr = child.get();
auto control = luma::scroll_view(std::unique_ptr<luma::control>{std::move(child)});
BOOST_REQUIRE(control);
auto measured = (*control)->measure(context, theme, {
.max = {
.width = 100.0F,
.height = 100.0F,
},
});
BOOST_TEST(measured.height == 100.0F);
(*control)->arrange({
.x = 0.0F,
.y = 0.0F,
.width = 100.0F,
.height = 100.0F,
});
(*control)->handle_touch({
.points = {
{
.id = 1,
.position = {
.x = 20.0F,
.y = 90.0F,
},
.phase = luma::touch_phase::DOWN,
},
},
});
(*control)->handle_touch({
.points = {
{
.id = 1,
.position = {
.x = 20.0F,
.y = -610.0F,
},
.phase = luma::touch_phase::MOVE,
},
},
});
(*control)->arrange({
.x = 0.0F,
.y = 0.0F,
.width = 100.0F,
.height = 100.0F,
});
BOOST_TEST(child_ptr->arranged.y <= -600.0F);
}
BOOST_AUTO_TEST_CASE(scroll_view_caches_measurement_and_rearranges_on_scroll_move) {
auto context = fake_draw_context{};
auto theme = luma::default_theme();
auto child = std::make_unique<counting_control>(luma::size{
.width = 20.0F,
.height = 1000.0F,
});
auto *child_ptr = child.get();
auto control = luma::scroll_view(std::unique_ptr<luma::control>{std::move(child)});
BOOST_REQUIRE(control);
auto measured = (*control)->measure(context, theme, {
.max = {
.width = 100.0F,
.height = 100.0F,
},
});
BOOST_TEST(measured.height == 100.0F);
measured = (*control)->measure(context, theme, {
.max = {
.width = 100.0F,
.height = 100.0F,
},
});
BOOST_TEST(measured.height == 100.0F);
BOOST_TEST(child_ptr->measure_count == 1);
(*control)->arrange({
.x = 0.0F,
.y = 0.0F,
.width = 100.0F,
.height = 100.0F,
});
(*control)->handle_touch({
.points = {
{
.id = 1,
.position = {
.x = 20.0F,
.y = 90.0F,
},
.phase = luma::touch_phase::DOWN,
},
},
});
(*control)->handle_touch({
.points = {
{
.id = 1,
.position = {
.x = 20.0F,
.y = 20.0F,
},
.phase = luma::touch_phase::MOVE,
},
},
});
BOOST_TEST(child_ptr->measure_count == 1);
BOOST_TEST(child_ptr->arranged.y < 0.0F);
(*control)->invalidate_layout();
measured = (*control)->measure(context, theme, {
.max = {
.width = 100.0F,
.height = 100.0F,
},
});
BOOST_TEST(measured.height == 100.0F);
BOOST_TEST(child_ptr->measure_count == 2);
BOOST_TEST(child_ptr->invalidate_count >= 1);
}
BOOST_AUTO_TEST_CASE(interpolate_validates_public_inputs) {
auto linear = luma::interpolate(0.0F, 10.0F, 0.5F, luma::easing::LINEAR);
BOOST_REQUIRE(linear);
BOOST_TEST(*linear == 5.0F);
auto clamped = luma::interpolate(0.0F, 10.0F, 2.0F, luma::easing::EASE_OUT);
BOOST_REQUIRE(clamped);
BOOST_TEST(*clamped == 10.0F);
auto invalid_from = luma::interpolate(std::numeric_limits<std::float32_t>::quiet_NaN(), 10.0F, 0.5F, luma::easing::LINEAR);
BOOST_REQUIRE(!invalid_from);
BOOST_TEST(invalid_from.error().code() == 1);
auto invalid_progress = luma::interpolate(0.0F, 10.0F, std::numeric_limits<std::float32_t>::infinity(), luma::easing::LINEAR);
BOOST_REQUIRE(!invalid_progress);
BOOST_TEST(invalid_progress.error().code() == 1);
}
BOOST_AUTO_TEST_CASE(animation_graph_validates_inputs_and_converts_callback_exceptions) {
auto graph = luma::animation_graph{};
auto invalid = graph.add({
.from = 0.0F,
.to = 1.0F,
.duration = std::chrono::milliseconds{0},
.apply = [](std::float32_t) {},
});
BOOST_REQUIRE(!invalid);
BOOST_TEST(invalid.error().code() == 1);
BOOST_REQUIRE(graph.add({
.from = 0.0F,
.to = 1.0F,
.duration = std::chrono::milliseconds{1},
.apply = [](std::float32_t) {
throw std::runtime_error{"boom"};
},
}));
auto status = graph.tick(std::chrono::milliseconds{1});
BOOST_REQUIRE(!status);
BOOST_TEST(status.error().code() == 5);
}
BOOST_AUTO_TEST_CASE(animation_graph_defers_reentrant_add_and_clear) {
auto graph = luma::animation_graph{};
auto added_applied = 0;
BOOST_REQUIRE(graph.add({
.from = 0.0F,
.to = 1.0F,
.duration = std::chrono::milliseconds{10},
.apply = [&](std::float32_t) {
BOOST_REQUIRE(graph.add({
.from = 0.0F,
.to = 1.0F,
.duration = std::chrono::milliseconds{10},
.apply = [&](std::float32_t) { ++added_applied; },
}));
},
}));
BOOST_REQUIRE(graph.tick(std::chrono::milliseconds{5}));
BOOST_REQUIRE(graph.tick(std::chrono::milliseconds{5}));
BOOST_TEST(added_applied == 1);
graph.clear();
auto second_applied = false;
BOOST_REQUIRE(graph.add({
.from = 0.0F,
.to = 1.0F,
.duration = std::chrono::milliseconds{10},
.apply = [&](std::float32_t) { graph.clear(); },
}));
BOOST_REQUIRE(graph.add({
.from = 0.0F,
.to = 1.0F,
.duration = std::chrono::milliseconds{10},
.apply = [&](std::float32_t) { second_applied = true; },
}));
BOOST_REQUIRE(graph.tick(std::chrono::milliseconds{5}));
BOOST_TEST(!second_applied);
BOOST_TEST(graph.empty());
}
BOOST_AUTO_TEST_CASE(dispatch_allows_recursive_calls_before_run) {
auto app = luma::application{};
auto result = app.dispatch([&app] -> std::expected<void, modulus::error> {
return app.dispatch([] -> std::expected<void, modulus::error> {
return {};
});
});
BOOST_REQUIRE(result);
}
BOOST_AUTO_TEST_CASE(config_parsing_checks_integer_range_and_accepts_integer_theme_numbers) {
auto invalid = luma::parse_display_config(boost::json::parse(R"({"width": 2147483648, "height": 600, "target_fps": 60})"));
BOOST_TEST(!invalid);
auto theme = luma::parse_theme(boost::json::parse(R"({"body": {"size": 24, "line_height": 30}, "radius": 4, "spacing": 8})"));
BOOST_REQUIRE(theme);
BOOST_TEST(theme->body.size == 24.0F);
BOOST_TEST(theme->body.line_height == 30.0F);
BOOST_TEST(theme->radius == 4.0F);
BOOST_TEST(theme->spacing == 8.0F);
auto config = luma::parse_application_config(boost::json::parse(R"({"title": "Scale", "display": {"width": 1024, "height": 600, "target_fps": 30, "idle_fps": 10, "fullscreen": false}, "touch": {"max_points": 5}, "theme": {"body": {"font": "default", "size": 24, "line_height": 30}, "radius": 4, "spacing": 8}})"));
BOOST_REQUIRE(config);
auto round_trip = luma::parse_application_config(luma::to_json(*config));
BOOST_REQUIRE(round_trip);
BOOST_TEST(round_trip->title == "Scale");
BOOST_TEST(round_trip->display.target_fps == 30);
BOOST_TEST(round_trip->display.idle_fps == 10);
BOOST_TEST(!round_trip->display.fullscreen);
BOOST_TEST(round_trip->theme.radius == 4.0F);
auto invalid_idle = luma::parse_display_config(boost::json::parse(R"({"target_fps": 30, "idle_fps": 31})"));
BOOST_REQUIRE(!invalid_idle);
BOOST_TEST(invalid_idle.error().describe().find("display.idle_fps must be less than or equal to display.target_fps") != std::string::npos);
auto zero_idle = luma::parse_display_config(boost::json::parse(R"({"target_fps": 30, "idle_fps": 0})"));
BOOST_REQUIRE(!zero_idle);
BOOST_TEST(zero_idle.error().describe().find("display.idle_fps must be positive") != std::string::npos);
auto unknown = luma::parse_application_config(boost::json::parse(R"({"title":"Scale","displayz":{}})"));
BOOST_REQUIRE(!unknown);
BOOST_TEST(unknown.error().describe().find("application config has unknown key: displayz") != std::string::npos);
auto unknown_theme = luma::parse_theme(boost::json::parse(R"({"body":{"size":24,"weight":"bold"}})"));
BOOST_REQUIRE(!unknown_theme);
BOOST_TEST(unknown_theme.error().describe().find("body has unknown key: weight") != std::string::npos);
auto empty_font_theme = luma::parse_theme(boost::json::parse(R"({"body":{"font":"","size":24,"line_height":30}})"));
BOOST_REQUIRE(!empty_font_theme);
BOOST_TEST(empty_font_theme.error().describe().find("text style font must not be empty") != std::string::npos);
}