2487 lines
83 KiB
C++
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);
|
|
}
|