#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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 filled{}; std::vector clips{}; std::vector drawn_styles{}; std::vector 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, modulus::error> override; }; struct reentrant_resources final : public luma::renderer_resources { auto load_texture(std::string const& path) -> std::expected, 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, 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, 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 phases{}; std::vector 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 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 state_{}; }; class closing_window_control final : public luma::control { public: closing_window_control(luma::window_manager& manager, std::uint64_t& id, std::shared_ptr 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 state_{}; }; class opening_window_control final : public luma::control { public: opening_window_control(luma::window_manager& manager, std::shared_ptr 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 state_{}; }; class updating_window_control final : public luma::control { public: updating_window_control(luma::window_manager& manager, std::uint64_t& id, std::shared_ptr 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 state_{}; }; class replacing_root_invalidate_control final : public luma::control { public: replacing_root_invalidate_control(luma::application& app, std::shared_ptr 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 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 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 state_{}; }; class closing_dialog_control final : public luma::control { public: closing_dialog_control(luma::overlay_host& host, std::shared_ptr 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 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(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, modulus::error> { return modulus::success(std::make_shared()); } auto reentrant_resources::load_texture(std::string const& path) -> std::expected, 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()); } auto same_path_reentrant_resources::load_texture(std::string const& path) -> std::expected, 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()); } auto blocking_resources::load_texture(std::string const&) -> std::expected, 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()); } 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 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 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 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()); 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 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 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::CANCELLED); state_->status_ok = status.has_value(); state_->in_handle = false; } replacing_root_invalidate_control::replacing_root_invalidate_control(luma::application& app, std::shared_ptr 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{std::make_unique()}; 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 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(); auto retired = std::vector>{}; router.set_retire_handler([&retired](std::unique_ptr view) { retired.push_back(std::move(view)); }); BOOST_REQUIRE(router.register_route("first", [state] { return modulus::success(std::unique_ptr{std::make_unique(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{}; auto threads = std::vector{}; for (auto index = 0; index < 16; ++index) { threads.emplace_back([&app, &values, index] { auto result = app.dispatch([&values, index] -> std::expected { 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); } 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(); auto root = std::unique_ptr{std::make_unique(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(luma::size{ .width = 10.0F, .height = 10.0F, }))); auto scroller = luma::scroll_view(std::make_unique(luma::size{ .width = 10.0F, .height = 100.0F, })); BOOST_REQUIRE(scroller); auto children = luma::children(std::make_unique(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(0, 16))); BOOST_TEST(!luma::scroll_view(std::unique_ptr{})); auto children = std::vector>{}; 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(4, 0); auto textures = std::vector>(4); auto threads = std::vector{}; 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(text_input_state_filters_modes_and_keyboard_updates_bound_field) { auto changes = std::vector{}; 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(result) == static_cast(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(result) == static_cast(luma::dialog_result::CANCELLED)); } 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(result) == static_cast(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(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::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(); auto body = std::make_unique(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(); 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(content_ptr->phases.front()) == static_cast(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(content_ptr->phases.back()) == static_cast(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()); 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(); 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(); 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(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()); 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()); 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(index + 20), .y = 0.0F, .width = 10.0F, .height = 10.0F, }, }, std::make_unique()); 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(); 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(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(); 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(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(); auto opened = manager.open({ .title = "Tool", .bounds = { .x = 100.0F, .y = 100.0F, .width = 100.0F, .height = 100.0F, }, }, std::make_unique(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(); 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(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()); 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(luma::size{ .width = 50.0F, .height = 50.0F, }); auto* first_ptr = first.get(); auto second = std::make_unique(luma::size{ .width = 50.0F, .height = 50.0F, }); auto* second_ptr = second.get(); auto control = luma::row(luma::children(modulus::success(std::unique_ptr{std::move(first)}), modulus::success(std::unique_ptr{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(luma::size{ .width = 20.0F, .height = 10.0F, }); auto* first_ptr = first.get(); auto second = std::make_unique(luma::size{ .width = 20.0F, .height = 20.0F, }); auto* second_ptr = second.get(); auto row = luma::row(luma::children(modulus::success(std::unique_ptr{std::move(first)}), modulus::success(std::unique_ptr{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(luma::size{ .width = 20.0F, .height = 10.0F, }); auto* child_ptr = child.get(); auto column = luma::column(luma::children(modulus::success(std::unique_ptr{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(luma::size{ .width = 20.0F, .height = 10.0F, }); auto control = luma::panel(luma::children(modulus::success(std::unique_ptr{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(luma::size{ .width = 20.0F, .height = 1000.0F, }); auto* child_ptr = child.get(); auto control = luma::scroll_view(std::unique_ptr{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(luma::size{ .width = 20.0F, .height = 1000.0F, }); auto* child_ptr = child.get(); auto control = luma::scroll_view(std::unique_ptr{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::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::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 { return app.dispatch([] -> std::expected { 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); }