This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
#include <luma/animation.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <expected>
|
||||
#include <exception>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include <modulus.hpp>
|
||||
|
||||
#include <luma/detail/errors.hpp>
|
||||
|
||||
namespace {
|
||||
|
||||
auto invalid(std::string message) -> std::expected<void, modulus::error> {
|
||||
return luma::detail::fail<void>(luma::detail::error_invalid_argument, std::move(message));
|
||||
}
|
||||
|
||||
template <typename VALUE>
|
||||
auto invalid_value(std::string message) -> std::expected<VALUE, modulus::error> {
|
||||
return luma::detail::fail<VALUE>(luma::detail::error_invalid_argument, std::move(message));
|
||||
}
|
||||
|
||||
auto internal(std::string message) -> std::expected<void, modulus::error> {
|
||||
return luma::detail::fail<void>(luma::detail::error_internal, std::move(message));
|
||||
}
|
||||
|
||||
auto validate(luma::animation const& animation) -> std::expected<void, modulus::error> {
|
||||
if (!std::isfinite(animation.from)) {
|
||||
return invalid("animation from value must be finite");
|
||||
}
|
||||
if (!std::isfinite(animation.to)) {
|
||||
return invalid("animation to value must be finite");
|
||||
}
|
||||
if (animation.duration.count() <= 0) {
|
||||
return invalid("animation duration must be positive");
|
||||
}
|
||||
if (!animation.apply) {
|
||||
return invalid("animation apply callback must not be empty");
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto callback_error(std::char_t const* action, std::exception const& error) -> std::expected<void, modulus::error> {
|
||||
return internal(std::string{"animation "} + action + " callback failed: " + error.what());
|
||||
}
|
||||
|
||||
auto callback_error(std::char_t const* action) -> std::expected<void, modulus::error> {
|
||||
return internal(std::string{"animation "} + action + " callback failed with an unknown exception");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
auto luma::interpolate(std::float32_t from, std::float32_t to, std::float32_t progress, luma::easing easing) -> std::expected<std::float32_t, modulus::error> {
|
||||
if (!std::isfinite(from)) {
|
||||
return invalid_value<std::float32_t>("interpolation from value must be finite");
|
||||
}
|
||||
if (!std::isfinite(to)) {
|
||||
return invalid_value<std::float32_t>("interpolation to value must be finite");
|
||||
}
|
||||
if (!std::isfinite(progress)) {
|
||||
return invalid_value<std::float32_t>("interpolation progress must be finite");
|
||||
}
|
||||
auto clamped = std::clamp(progress, std::float32_t{}, std::float32_t{1});
|
||||
auto eased = clamped;
|
||||
switch (easing) {
|
||||
case luma::easing::LINEAR:
|
||||
eased = clamped;
|
||||
break;
|
||||
case luma::easing::EASE_OUT:
|
||||
eased = 1.0F - (1.0F - clamped) * (1.0F - clamped);
|
||||
break;
|
||||
case luma::easing::EASE_IN_OUT:
|
||||
eased = clamped < 0.5F ? 2.0F * clamped * clamped : 1.0F - std::pow(-2.0F * clamped + 2.0F, 2.0F) / 2.0F;
|
||||
break;
|
||||
}
|
||||
return modulus::success(from + (to - from) * eased);
|
||||
}
|
||||
|
||||
auto luma::animation_graph::add(luma::animation animation) -> std::expected<void, modulus::error> {
|
||||
if (auto status = validate(animation); !status) {
|
||||
return status;
|
||||
}
|
||||
auto running = luma::animation_graph::running_animation{
|
||||
.animation = std::move(animation),
|
||||
.elapsed = {},
|
||||
};
|
||||
if (ticking_) {
|
||||
pending_.push_back({
|
||||
.kind = luma::animation_graph::pending_kind::ADD,
|
||||
.animation = std::move(running),
|
||||
});
|
||||
} else {
|
||||
animations_.push_back(std::move(running));
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto luma::animation_graph::tick(std::chrono::steady_clock::duration delta) -> std::expected<void, modulus::error> {
|
||||
if (delta < std::chrono::steady_clock::duration::zero()) {
|
||||
return invalid("animation tick delta must be non-negative");
|
||||
}
|
||||
if (ticking_) {
|
||||
return internal("animation graph is already ticking");
|
||||
}
|
||||
|
||||
auto active = std::move(animations_);
|
||||
animations_.clear();
|
||||
ticking_ = true;
|
||||
clear_requested_ = false;
|
||||
|
||||
auto fail_tick = [this](std::expected<void, modulus::error> status) {
|
||||
ticking_ = false;
|
||||
clear_requested_ = false;
|
||||
animations_.clear();
|
||||
pending_.clear();
|
||||
return status;
|
||||
};
|
||||
|
||||
for (auto& running : active) {
|
||||
running.elapsed += delta;
|
||||
auto duration = std::chrono::duration_cast<std::chrono::steady_clock::duration>(running.animation.duration);
|
||||
if (duration.count() <= 0) {
|
||||
return fail_tick(invalid("animation duration must be positive"));
|
||||
}
|
||||
auto progress = static_cast<std::float32_t>(running.elapsed.count()) / static_cast<std::float32_t>(duration.count());
|
||||
progress = std::clamp(progress, std::float32_t{}, std::float32_t{1});
|
||||
if (!running.animation.apply) {
|
||||
return fail_tick(invalid("animation apply callback must not be empty"));
|
||||
}
|
||||
auto interpolated = luma::interpolate(running.animation.from, running.animation.to, progress, running.animation.easing);
|
||||
if (!interpolated) {
|
||||
return fail_tick(modulus::fail<void>(interpolated.error()));
|
||||
}
|
||||
try {
|
||||
running.animation.apply(*interpolated);
|
||||
} catch (std::exception const& error) {
|
||||
return fail_tick(callback_error("apply", error));
|
||||
} catch (...) {
|
||||
return fail_tick(callback_error("apply"));
|
||||
}
|
||||
if (clear_requested_) {
|
||||
break;
|
||||
}
|
||||
if (progress >= 1.0F) {
|
||||
if (running.animation.finished) {
|
||||
try {
|
||||
running.animation.finished();
|
||||
} catch (std::exception const& error) {
|
||||
return fail_tick(callback_error("finished", error));
|
||||
} catch (...) {
|
||||
return fail_tick(callback_error("finished"));
|
||||
}
|
||||
}
|
||||
if (clear_requested_) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
animations_.push_back(std::move(running));
|
||||
}
|
||||
|
||||
ticking_ = false;
|
||||
for (auto& operation : pending_) {
|
||||
if (operation.kind == luma::animation_graph::pending_kind::CLEAR) {
|
||||
animations_.clear();
|
||||
} else {
|
||||
animations_.push_back(std::move(operation.animation));
|
||||
}
|
||||
}
|
||||
pending_.clear();
|
||||
clear_requested_ = false;
|
||||
return {};
|
||||
}
|
||||
|
||||
auto luma::animation_graph::empty() const -> std::bool_t {
|
||||
return animations_.empty();
|
||||
}
|
||||
|
||||
auto luma::animation_graph::clear() -> void {
|
||||
if (ticking_) {
|
||||
pending_.push_back({
|
||||
.kind = luma::animation_graph::pending_kind::CLEAR,
|
||||
.animation = {},
|
||||
});
|
||||
clear_requested_ = true;
|
||||
} else {
|
||||
animations_.clear();
|
||||
pending_.clear();
|
||||
clear_requested_ = false;
|
||||
}
|
||||
}
|
||||
+1264
File diff suppressed because it is too large
Load Diff
+126
@@ -0,0 +1,126 @@
|
||||
#include <luma/assets.hpp>
|
||||
|
||||
#include <cassert>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <exception>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
#include <modulus.hpp>
|
||||
#include <modulus/log.hpp>
|
||||
|
||||
#include <luma/detail/errors.hpp>
|
||||
|
||||
struct luma::asset_cache::in_flight_load {
|
||||
std::condition_variable ready{};
|
||||
std::thread::id owner{};
|
||||
std::expected<std::shared_ptr<luma::texture>, modulus::error> result{};
|
||||
std::bool_t done{false};
|
||||
};
|
||||
|
||||
luma::asset_cache::asset_cache(luma::renderer_resources& resources) : resources_{&resources} {
|
||||
assert(resources_ != nullptr);
|
||||
}
|
||||
|
||||
auto luma::asset_cache::texture(std::string const& path) -> std::expected<std::shared_ptr<luma::texture>, modulus::error> {
|
||||
assert(resources_ != nullptr);
|
||||
if (path.empty()) {
|
||||
return luma::detail::fail<std::shared_ptr<luma::texture>>(luma::detail::error_invalid_argument, "texture path must not be empty");
|
||||
}
|
||||
auto generation = std::uint64_t{};
|
||||
auto loading = std::shared_ptr<luma::asset_cache::in_flight_load>{};
|
||||
{
|
||||
auto lock = std::unique_lock{mutex_};
|
||||
if (auto found = textures_.find(path); found != textures_.end()) {
|
||||
assert(found->second != nullptr);
|
||||
return modulus::success(found->second);
|
||||
}
|
||||
if (auto found = loading_.find(path); found != loading_.end()) {
|
||||
loading = found->second;
|
||||
assert(loading != nullptr);
|
||||
if (loading->owner == std::this_thread::get_id()) {
|
||||
return luma::detail::fail<std::shared_ptr<luma::texture>>(luma::detail::error_unavailable, "texture load reentered for path: " + path);
|
||||
}
|
||||
loading->ready.wait(lock, [&loading] {
|
||||
return loading->done;
|
||||
});
|
||||
if (auto cached = textures_.find(path); cached != textures_.end()) {
|
||||
assert(cached->second != nullptr);
|
||||
return modulus::success(cached->second);
|
||||
}
|
||||
if (!loading->result) {
|
||||
return modulus::fail<std::shared_ptr<luma::texture>>(loading->result.error());
|
||||
}
|
||||
assert(*loading->result != nullptr);
|
||||
return modulus::success(*loading->result);
|
||||
}
|
||||
loading = std::make_shared<luma::asset_cache::in_flight_load>();
|
||||
loading->owner = std::this_thread::get_id();
|
||||
loading_.emplace(path, loading);
|
||||
generation = generation_;
|
||||
}
|
||||
auto loaded = std::expected<std::shared_ptr<luma::texture>, modulus::error>{};
|
||||
modulus::log_debug("luma texture loading: " + path);
|
||||
try {
|
||||
loaded = resources_->load_texture(path);
|
||||
} catch (std::exception const& error) {
|
||||
loaded = luma::detail::fail<std::shared_ptr<luma::texture>>(luma::detail::error_internal, std::string{"texture load failed: "} + error.what());
|
||||
} catch (...) {
|
||||
loaded = luma::detail::fail<std::shared_ptr<luma::texture>>(luma::detail::error_internal, "texture load failed with an unknown exception");
|
||||
}
|
||||
if (!loaded) {
|
||||
modulus::log_debug("luma texture load failed: " + path);
|
||||
auto lock = std::scoped_lock{mutex_};
|
||||
loading->result = modulus::fail<std::shared_ptr<luma::texture>>(loaded.error());
|
||||
loading->done = true;
|
||||
loading_.erase(path);
|
||||
loading->ready.notify_all();
|
||||
return loaded;
|
||||
}
|
||||
if (*loaded == nullptr) {
|
||||
modulus::log_debug("luma texture load returned null: " + path);
|
||||
auto result = luma::detail::fail<std::shared_ptr<luma::texture>>(luma::detail::error_not_found, "texture load returned no texture: " + path);
|
||||
auto lock = std::scoped_lock{mutex_};
|
||||
loading->result = modulus::fail<std::shared_ptr<luma::texture>>(result.error());
|
||||
loading->done = true;
|
||||
loading_.erase(path);
|
||||
loading->ready.notify_all();
|
||||
return result;
|
||||
}
|
||||
auto texture = *loaded;
|
||||
auto lock = std::scoped_lock{mutex_};
|
||||
if (auto found = textures_.find(path); found != textures_.end()) {
|
||||
assert(found->second != nullptr);
|
||||
loading->result = modulus::success(found->second);
|
||||
loading->done = true;
|
||||
loading_.erase(path);
|
||||
loading->ready.notify_all();
|
||||
return modulus::success(found->second);
|
||||
}
|
||||
if (generation != generation_) {
|
||||
loading->result = modulus::success(texture);
|
||||
loading->done = true;
|
||||
loading_.erase(path);
|
||||
loading->ready.notify_all();
|
||||
return modulus::success(std::move(texture));
|
||||
}
|
||||
auto inserted = textures_.emplace(path, std::move(texture));
|
||||
assert(inserted.first->second != nullptr);
|
||||
loading->result = modulus::success(inserted.first->second);
|
||||
loading->done = true;
|
||||
loading_.erase(path);
|
||||
loading->ready.notify_all();
|
||||
return modulus::success(inserted.first->second);
|
||||
}
|
||||
|
||||
auto luma::asset_cache::clear() -> void {
|
||||
assert(resources_ != nullptr);
|
||||
auto lock = std::scoped_lock{mutex_};
|
||||
++generation_;
|
||||
textures_.clear();
|
||||
}
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
#include <luma/config.hpp>
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
|
||||
#include <modulus.hpp>
|
||||
|
||||
#include <luma/detail/errors.hpp>
|
||||
#include <luma/theme.hpp>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto json_options = modulus::json_config::options{
|
||||
.category = "luma",
|
||||
.invalid_argument_code = luma::detail::error_invalid_argument,
|
||||
.not_found_code = luma::detail::error_not_found
|
||||
};
|
||||
|
||||
auto invalid_display(std::string message) -> std::expected<luma::display_config, modulus::error> {
|
||||
assert(!message.empty());
|
||||
return luma::detail::fail<luma::display_config>(luma::detail::error_invalid_argument, std::move(message));
|
||||
}
|
||||
|
||||
auto invalid_touch(std::string message) -> std::expected<luma::touch_config, modulus::error> {
|
||||
assert(!message.empty());
|
||||
return luma::detail::fail<luma::touch_config>(luma::detail::error_invalid_argument, std::move(message));
|
||||
}
|
||||
|
||||
auto invalid_application(std::string message) -> std::expected<luma::application_config, modulus::error> {
|
||||
assert(!message.empty());
|
||||
return luma::detail::fail<luma::application_config>(luma::detail::error_invalid_argument, std::move(message));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
auto luma::validate(luma::display_config const& config) -> std::expected<void, modulus::error> {
|
||||
if (config.width <= 0) {
|
||||
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "display.width must be positive");
|
||||
}
|
||||
if (config.height <= 0) {
|
||||
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "display.height must be positive");
|
||||
}
|
||||
if (config.target_fps <= 0) {
|
||||
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "display.target_fps must be positive");
|
||||
}
|
||||
if (config.idle_fps <= 0) {
|
||||
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "display.idle_fps must be positive");
|
||||
}
|
||||
if (config.idle_fps > config.target_fps) {
|
||||
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "display.idle_fps must be less than or equal to display.target_fps");
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto luma::validate(luma::touch_config const& config) -> std::expected<void, modulus::error> {
|
||||
if (config.max_points <= 0) {
|
||||
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "touch.max_points must be positive");
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto luma::validate(luma::application_config const& config) -> std::expected<void, modulus::error> {
|
||||
if (config.title.empty()) {
|
||||
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "title must not be empty");
|
||||
}
|
||||
if (auto status = luma::validate(config.display); !status) {
|
||||
return modulus::fail<void>(status.error());
|
||||
}
|
||||
if (auto status = luma::validate(config.touch); !status) {
|
||||
return modulus::fail<void>(status.error());
|
||||
}
|
||||
if (auto status = luma::validate(config.theme); !status) {
|
||||
return modulus::fail<void>(status.error());
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto luma::parse_display_config(boost::json::value const& value) -> std::expected<luma::display_config, modulus::error> {
|
||||
auto object = modulus::json_config::require_object(value, "display", json_options);
|
||||
if (!object) {
|
||||
return invalid_display(std::string{object.error().message()});
|
||||
}
|
||||
auto unknown = modulus::json_config::reject_unknown_fields(**object, {
|
||||
"width",
|
||||
"height",
|
||||
"target_fps",
|
||||
"idle_fps",
|
||||
"fullscreen",
|
||||
}, "display", json_options);
|
||||
if (!unknown) {
|
||||
return invalid_display(std::string{unknown.error().message()});
|
||||
}
|
||||
auto config = luma::display_config{};
|
||||
auto width = modulus::json_config::optional_int32_field(**object, "width", config.width, std::numeric_limits<std::int32_t>::min(), std::numeric_limits<std::int32_t>::max(), json_options);
|
||||
auto height = modulus::json_config::optional_int32_field(**object, "height", config.height, std::numeric_limits<std::int32_t>::min(), std::numeric_limits<std::int32_t>::max(), json_options);
|
||||
auto target_fps = modulus::json_config::optional_int32_field(**object, "target_fps", config.target_fps, std::numeric_limits<std::int32_t>::min(), std::numeric_limits<std::int32_t>::max(), json_options);
|
||||
auto idle_fps = modulus::json_config::optional_int32_field(**object, "idle_fps", config.idle_fps, std::numeric_limits<std::int32_t>::min(), std::numeric_limits<std::int32_t>::max(), json_options);
|
||||
auto fullscreen = modulus::json_config::optional_bool_field(**object, "fullscreen", config.fullscreen, json_options);
|
||||
if (!width) {
|
||||
return invalid_display(std::string{width.error().message()});
|
||||
}
|
||||
if (!height) {
|
||||
return invalid_display(std::string{height.error().message()});
|
||||
}
|
||||
if (!target_fps) {
|
||||
return invalid_display(std::string{target_fps.error().message()});
|
||||
}
|
||||
if (!idle_fps) {
|
||||
return invalid_display(std::string{idle_fps.error().message()});
|
||||
}
|
||||
if (!fullscreen) {
|
||||
return invalid_display(std::string{fullscreen.error().message()});
|
||||
}
|
||||
config.width = *width;
|
||||
config.height = *height;
|
||||
config.target_fps = *target_fps;
|
||||
config.idle_fps = *idle_fps;
|
||||
config.fullscreen = *fullscreen;
|
||||
if (auto status = luma::validate(config); !status) {
|
||||
return invalid_display(std::string{status.error().message()});
|
||||
}
|
||||
return modulus::success(config);
|
||||
}
|
||||
|
||||
auto luma::parse_touch_config(boost::json::value const& value) -> std::expected<luma::touch_config, modulus::error> {
|
||||
auto object = modulus::json_config::require_object(value, "touch", json_options);
|
||||
if (!object) {
|
||||
return invalid_touch(std::string{object.error().message()});
|
||||
}
|
||||
auto unknown = modulus::json_config::reject_unknown_fields(**object, {"max_points"}, "touch", json_options);
|
||||
if (!unknown) {
|
||||
return invalid_touch(std::string{unknown.error().message()});
|
||||
}
|
||||
auto config = luma::touch_config{};
|
||||
auto max_points = modulus::json_config::optional_int32_field(**object, "max_points", config.max_points, std::numeric_limits<std::int32_t>::min(), std::numeric_limits<std::int32_t>::max(), json_options);
|
||||
if (!max_points) {
|
||||
return invalid_touch(std::string{max_points.error().message()});
|
||||
}
|
||||
config.max_points = *max_points;
|
||||
if (auto status = luma::validate(config); !status) {
|
||||
return invalid_touch(std::string{status.error().message()});
|
||||
}
|
||||
return modulus::success(config);
|
||||
}
|
||||
|
||||
auto luma::parse_application_config(boost::json::value const& value) -> std::expected<luma::application_config, modulus::error> {
|
||||
auto object = modulus::json_config::require_object(value, "application config", json_options);
|
||||
if (!object) {
|
||||
return invalid_application(std::string{object.error().message()});
|
||||
}
|
||||
auto unknown = modulus::json_config::reject_unknown_fields(**object, {
|
||||
"title",
|
||||
"display",
|
||||
"touch",
|
||||
"theme",
|
||||
}, "application config", json_options);
|
||||
if (!unknown) {
|
||||
return invalid_application(std::string{unknown.error().message()});
|
||||
}
|
||||
auto config = luma::application_config{};
|
||||
auto title = modulus::json_config::optional_string_field(**object, "title", config.title, json_options);
|
||||
if (!title) {
|
||||
return invalid_application(std::string{title.error().message()});
|
||||
}
|
||||
config.title = *title;
|
||||
if (auto const* display = (*object)->if_contains("display")) {
|
||||
auto parsed = luma::parse_display_config(*display);
|
||||
if (!parsed) {
|
||||
return invalid_application(std::string{parsed.error().message()});
|
||||
}
|
||||
config.display = *parsed;
|
||||
}
|
||||
if (auto const* touch = (*object)->if_contains("touch")) {
|
||||
auto parsed = luma::parse_touch_config(*touch);
|
||||
if (!parsed) {
|
||||
return invalid_application(std::string{parsed.error().message()});
|
||||
}
|
||||
config.touch = *parsed;
|
||||
}
|
||||
if (auto const* theme = (*object)->if_contains("theme")) {
|
||||
auto parsed = luma::parse_theme(*theme);
|
||||
if (!parsed) {
|
||||
return invalid_application(std::string{parsed.error().message()});
|
||||
}
|
||||
config.theme = *parsed;
|
||||
}
|
||||
if (auto status = luma::validate(config); !status) {
|
||||
return invalid_application(std::string{status.error().message()});
|
||||
}
|
||||
return modulus::success(config);
|
||||
}
|
||||
|
||||
auto luma::load_application_config(std::string const& path) -> std::expected<luma::application_config, modulus::error> {
|
||||
auto value = modulus::json_config::load_value(path, json_options);
|
||||
if (!value) {
|
||||
return modulus::fail<luma::application_config>(value.error());
|
||||
}
|
||||
return luma::parse_application_config(*value);
|
||||
}
|
||||
|
||||
auto luma::to_json(luma::display_config const& config) -> boost::json::value {
|
||||
return boost::json::object{
|
||||
{
|
||||
"width",
|
||||
config.width,
|
||||
},
|
||||
{
|
||||
"height",
|
||||
config.height,
|
||||
},
|
||||
{
|
||||
"target_fps",
|
||||
config.target_fps,
|
||||
},
|
||||
{
|
||||
"idle_fps",
|
||||
config.idle_fps,
|
||||
},
|
||||
{
|
||||
"fullscreen",
|
||||
config.fullscreen,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
auto luma::to_json(luma::touch_config const& config) -> boost::json::value {
|
||||
return boost::json::object{
|
||||
{
|
||||
"max_points",
|
||||
config.max_points,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
auto luma::to_json(luma::application_config const& config) -> boost::json::value {
|
||||
return boost::json::object{
|
||||
{
|
||||
"title",
|
||||
config.title,
|
||||
},
|
||||
{
|
||||
"display",
|
||||
luma::to_json(config.display),
|
||||
},
|
||||
{
|
||||
"touch",
|
||||
luma::to_json(config.touch),
|
||||
},
|
||||
{
|
||||
"theme",
|
||||
luma::to_json(config.theme),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
#include <luma/control.hpp>
|
||||
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
|
||||
#include <modulus.hpp>
|
||||
|
||||
namespace {
|
||||
|
||||
auto valid_rect(luma::rect value) -> std::bool_t {
|
||||
return std::isfinite(value.x) && std::isfinite(value.y) && std::isfinite(value.width) && std::isfinite(value.height) && value.width >= 0.0F && value.height >= 0.0F;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
luma::control::~control() = default;
|
||||
|
||||
auto luma::control::arrange(luma::rect bounds) -> void {
|
||||
assert(valid_rect(bounds));
|
||||
bounds_ = bounds;
|
||||
}
|
||||
|
||||
auto luma::control::handle_touch(luma::touch_event const& event) -> std::bool_t {
|
||||
static_cast<void>(event);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto luma::control::invalidate_layout() -> void {}
|
||||
|
||||
auto luma::control::bind_layout_invalidator(std::function<void()> invalidator) -> void {
|
||||
layout_invalidator_ = std::move(invalidator);
|
||||
}
|
||||
|
||||
auto luma::control::focusable() const -> std::bool_t {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto luma::control::set_focused(std::bool_t value) -> void {
|
||||
assert(!value || focusable());
|
||||
focused_ = value;
|
||||
}
|
||||
|
||||
auto luma::control::focused() const -> std::bool_t {
|
||||
return focused_;
|
||||
}
|
||||
|
||||
auto luma::control::set_enabled(std::bool_t value) -> void {
|
||||
enabled_ = value;
|
||||
if (!enabled_) {
|
||||
focused_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
auto luma::control::enabled() const -> std::bool_t {
|
||||
return enabled_;
|
||||
}
|
||||
|
||||
auto luma::control::bounds() const -> luma::rect {
|
||||
assert(valid_rect(bounds_));
|
||||
return bounds_;
|
||||
}
|
||||
|
||||
auto luma::control::notify_layout_changed() const -> void {
|
||||
if (!layout_invalidator_) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
layout_invalidator_();
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
|
||||
auto luma::control::layout_invalidator() const -> std::function<void()> {
|
||||
return layout_invalidator_;
|
||||
}
|
||||
+1631
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
||||
#include <luma/detail/errors.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <utility>
|
||||
|
||||
#include <modulus.hpp>
|
||||
|
||||
auto luma::detail::make_error(std::int32_t code, std::string message, modulus::source_location location) -> modulus::error {
|
||||
return modulus::make_error("luma", code, std::move(message), location);
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
#include <luma/overlays.hpp>
|
||||
|
||||
#include <cassert>
|
||||
#include <expected>
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include <modulus.hpp>
|
||||
#include <modulus/log.hpp>
|
||||
|
||||
#include <luma/detail/errors.hpp>
|
||||
#include <luma/geometry.hpp>
|
||||
#include <luma/input.hpp>
|
||||
#include <luma/render.hpp>
|
||||
#include <luma/theme.hpp>
|
||||
|
||||
namespace {
|
||||
|
||||
class scope_exit {
|
||||
public:
|
||||
explicit scope_exit(std::function<void()> action);
|
||||
scope_exit(scope_exit const&) = delete;
|
||||
auto operator=(scope_exit const&) -> scope_exit& = delete;
|
||||
~scope_exit();
|
||||
private:
|
||||
std::function<void()> action_{};
|
||||
};
|
||||
|
||||
auto invalid(std::string message) -> std::expected<void, modulus::error> {
|
||||
return luma::detail::fail<void>(luma::detail::error_invalid_argument, std::move(message));
|
||||
}
|
||||
|
||||
auto unavailable(std::string message) -> std::expected<void, modulus::error> {
|
||||
return luma::detail::fail<void>(luma::detail::error_unavailable, std::move(message));
|
||||
}
|
||||
|
||||
template <typename VALUE>
|
||||
auto invalid_value(std::string message) -> std::expected<VALUE, modulus::error> {
|
||||
return luma::detail::fail<VALUE>(luma::detail::error_invalid_argument, std::move(message));
|
||||
}
|
||||
|
||||
auto dialog_bounds(luma::rect bounds) -> luma::rect {
|
||||
return {
|
||||
.x = bounds.x + bounds.width * 0.1F,
|
||||
.y = bounds.y + bounds.height * 0.15F,
|
||||
.width = bounds.width * 0.8F,
|
||||
.height = bounds.height * 0.7F,
|
||||
};
|
||||
}
|
||||
|
||||
auto dialog_body_bounds(luma::rect bounds) -> luma::rect {
|
||||
return luma::inset(dialog_bounds(bounds), {
|
||||
16.0F,
|
||||
56.0F,
|
||||
16.0F,
|
||||
72.0F,
|
||||
});
|
||||
}
|
||||
|
||||
auto accept_bounds(luma::rect bounds, std::bool_t has_cancel) -> luma::rect {
|
||||
auto dialog = dialog_bounds(bounds);
|
||||
auto width = 128.0F;
|
||||
auto x = has_cancel ? luma::right(dialog) - 16.0F - width : dialog.x + (dialog.width - width) * 0.5F;
|
||||
return {
|
||||
.x = x,
|
||||
.y = luma::bottom(dialog) - 56.0F,
|
||||
.width = width,
|
||||
.height = 40.0F,
|
||||
};
|
||||
}
|
||||
|
||||
auto cancel_bounds(luma::rect bounds) -> luma::rect {
|
||||
auto accept = accept_bounds(bounds, true);
|
||||
return {
|
||||
.x = accept.x - 136.0F,
|
||||
.y = accept.y,
|
||||
.width = 128.0F,
|
||||
.height = 40.0F,
|
||||
};
|
||||
}
|
||||
|
||||
auto draw_button(luma::draw_context& context, luma::theme const& theme, luma::rect bounds, std::string const& text, luma::color fill) -> void {
|
||||
context.fill_rect(bounds, fill);
|
||||
context.stroke_rect(bounds, 1.0F, theme.border);
|
||||
auto measured = context.measure_text(text, theme.body);
|
||||
context.draw_text(text, {
|
||||
.x = bounds.x + (bounds.width - measured.width) * 0.5F,
|
||||
.y = bounds.y + (bounds.height - measured.height) * 0.5F,
|
||||
}, theme.body, theme.text);
|
||||
}
|
||||
|
||||
auto first_point_inside(luma::touch_event const& event, luma::rect bounds, luma::touch_phase phase) -> luma::touch_point const* {
|
||||
for (auto const& point : event.points) {
|
||||
if (point.phase == phase && luma::contains(bounds, point.position)) {
|
||||
return &point;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto any_point_inside(luma::touch_event const& event, luma::rect bounds) -> std::bool_t {
|
||||
for (auto const& point : event.points) {
|
||||
if (luma::contains(bounds, point.position)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
auto throw_if_failed(std::expected<void, modulus::error> const& status) -> void {
|
||||
if (!status) {
|
||||
throw std::runtime_error(status.error().describe());
|
||||
}
|
||||
}
|
||||
|
||||
scope_exit::scope_exit(std::function<void()> action) : action_{std::move(action)} {}
|
||||
|
||||
scope_exit::~scope_exit() {
|
||||
if (action_) {
|
||||
action_();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
luma::overlay_host::overlay_host(std::unique_ptr<luma::control> content) : content_{std::move(content)} {
|
||||
assert(content_ != nullptr);
|
||||
}
|
||||
|
||||
auto luma::overlay_host::show_dialog(luma::dialog_options options) -> std::expected<void, modulus::error> {
|
||||
if (options.body == nullptr) {
|
||||
return invalid("dialog body must not be null");
|
||||
}
|
||||
if (options.accept_text.empty()) {
|
||||
return invalid("dialog accept text must not be empty");
|
||||
}
|
||||
options.body->bind_layout_invalidator(layout_invalidator());
|
||||
if (dialog_) {
|
||||
auto body = std::move(dialog_->body);
|
||||
dialog_.reset();
|
||||
retire_dialog_body(std::move(body));
|
||||
}
|
||||
dialog_ = std::move(options);
|
||||
modulus::log_debug("luma dialog shown");
|
||||
return {};
|
||||
}
|
||||
|
||||
auto luma::overlay_host::close_dialog(luma::dialog_result result) -> std::expected<void, modulus::error> {
|
||||
if (result == luma::dialog_result::NONE) {
|
||||
return invalid("dialog close result must not be NONE");
|
||||
}
|
||||
if (!dialog_) {
|
||||
return unavailable("no dialog is open");
|
||||
}
|
||||
auto callback = std::move(dialog_->on_close);
|
||||
auto body = std::move(dialog_->body);
|
||||
dialog_.reset();
|
||||
retire_dialog_body(std::move(body));
|
||||
modulus::log_debug("luma dialog closed");
|
||||
if (callback) {
|
||||
try {
|
||||
callback(result);
|
||||
} catch (std::exception const& error) {
|
||||
return luma::detail::fail<void>(luma::detail::error_internal, std::string{"dialog close callback failed: "} + error.what());
|
||||
} catch (...) {
|
||||
return luma::detail::fail<void>(luma::detail::error_internal, "dialog close callback failed with an unknown exception");
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto luma::overlay_host::begin_traversal() const -> void {
|
||||
++traversal_depth_;
|
||||
}
|
||||
|
||||
auto luma::overlay_host::end_traversal() const -> void {
|
||||
assert(traversal_depth_ > 0);
|
||||
--traversal_depth_;
|
||||
flush_retired_dialog_bodies();
|
||||
}
|
||||
|
||||
auto luma::overlay_host::retire_dialog_body(std::unique_ptr<luma::control> body) -> void {
|
||||
if (body == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (traversal_depth_ == 0) {
|
||||
body.reset();
|
||||
return;
|
||||
}
|
||||
retired_dialog_bodies_.push_back(std::move(body));
|
||||
}
|
||||
|
||||
auto luma::overlay_host::flush_retired_dialog_bodies() const -> void {
|
||||
if (traversal_depth_ == 0) {
|
||||
retired_dialog_bodies_.clear();
|
||||
}
|
||||
}
|
||||
|
||||
auto luma::overlay_host::show_toast(luma::toast toast) -> std::expected<void, modulus::error> {
|
||||
if (toast.message.empty()) {
|
||||
return invalid("toast message must not be empty");
|
||||
}
|
||||
if (toast.duration.count() <= 0) {
|
||||
return invalid("toast duration must be positive");
|
||||
}
|
||||
toasts_.push_back(std::move(toast));
|
||||
return {};
|
||||
}
|
||||
|
||||
auto luma::overlay_host::tick(std::chrono::steady_clock::duration delta) -> std::bool_t {
|
||||
assert(content_ != nullptr);
|
||||
assert(delta >= std::chrono::steady_clock::duration::zero());
|
||||
if (toasts_.empty()) {
|
||||
toast_elapsed_ = {};
|
||||
return false;
|
||||
}
|
||||
toast_elapsed_ += delta;
|
||||
if (toast_elapsed_ >= toasts_.front().duration) {
|
||||
toasts_.pop_front();
|
||||
toast_elapsed_ = {};
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
auto luma::overlay_host::measure(luma::draw_context& context, luma::theme const& theme, luma::constraints limits) -> luma::size {
|
||||
assert(content_ != nullptr);
|
||||
begin_traversal();
|
||||
auto guard = scope_exit{[this] {
|
||||
end_traversal();
|
||||
}};
|
||||
auto measured = content_->measure(context, theme, limits);
|
||||
if (dialog_ && dialog_->body) {
|
||||
auto* body = dialog_->body.get();
|
||||
body->measure(context, theme, {
|
||||
.min = {},
|
||||
.max = {
|
||||
.width = limits.max.width * 0.8F,
|
||||
.height = limits.max.height * 0.8F,
|
||||
},
|
||||
});
|
||||
}
|
||||
return measured;
|
||||
}
|
||||
|
||||
auto luma::overlay_host::arrange(luma::rect bounds) -> void {
|
||||
assert(content_ != nullptr);
|
||||
begin_traversal();
|
||||
auto guard = scope_exit{[this] {
|
||||
end_traversal();
|
||||
}};
|
||||
luma::control::arrange(bounds);
|
||||
content_->arrange(bounds);
|
||||
if (dialog_ && dialog_->body) {
|
||||
auto* body = dialog_->body.get();
|
||||
body->arrange(dialog_body_bounds(bounds));
|
||||
}
|
||||
}
|
||||
|
||||
auto luma::overlay_host::draw(luma::draw_context& context, luma::theme const& theme) const -> void {
|
||||
assert(content_ != nullptr);
|
||||
begin_traversal();
|
||||
auto guard = scope_exit{[this] {
|
||||
end_traversal();
|
||||
}};
|
||||
content_->draw(context, theme);
|
||||
if (dialog_ && dialog_->body) {
|
||||
auto dialog = dialog_bounds(bounds());
|
||||
auto title = dialog_->title;
|
||||
auto accept_text = dialog_->accept_text;
|
||||
auto cancel_text = dialog_->cancel_text;
|
||||
auto* body = dialog_->body.get();
|
||||
context.fill_rect(dialog, theme.surface);
|
||||
context.stroke_rect(dialog, 2.0F, theme.accent);
|
||||
context.draw_text(title, {
|
||||
.x = dialog.x + 16.0F,
|
||||
.y = dialog.y + 16.0F,
|
||||
}, theme.title, theme.text);
|
||||
body->draw(context, theme);
|
||||
auto has_cancel = !cancel_text.empty();
|
||||
if (has_cancel) {
|
||||
draw_button(context, theme, cancel_bounds(bounds()), cancel_text, theme.surface);
|
||||
}
|
||||
draw_button(context, theme, accept_bounds(bounds(), has_cancel), accept_text, theme.surface_active);
|
||||
}
|
||||
if (!toasts_.empty()) {
|
||||
auto toast_bounds = luma::rect{
|
||||
.x = bounds().x + bounds().width * 0.2F,
|
||||
.y = bounds().y + bounds().height - 72.0F,
|
||||
.width = bounds().width * 0.6F,
|
||||
.height = 48.0F,
|
||||
};
|
||||
context.fill_rect(toast_bounds, theme.surface_active);
|
||||
context.draw_text(toasts_.front().message, {
|
||||
.x = toast_bounds.x + 16.0F,
|
||||
.y = toast_bounds.y + 10.0F,
|
||||
}, theme.body, theme.text);
|
||||
}
|
||||
}
|
||||
|
||||
auto luma::overlay_host::handle_touch(luma::touch_event const& event) -> std::bool_t {
|
||||
assert(content_ != nullptr);
|
||||
begin_traversal();
|
||||
auto guard = scope_exit{[this] {
|
||||
end_traversal();
|
||||
}};
|
||||
if (dialog_) {
|
||||
if (event.points.empty()) {
|
||||
return true;
|
||||
}
|
||||
auto has_cancel = !dialog_->cancel_text.empty();
|
||||
if (first_point_inside(event, accept_bounds(bounds(), has_cancel), luma::touch_phase::UP) != nullptr) {
|
||||
throw_if_failed(close_dialog(luma::dialog_result::ACCEPTED));
|
||||
return true;
|
||||
}
|
||||
if (has_cancel && first_point_inside(event, cancel_bounds(bounds()), luma::touch_phase::UP) != nullptr) {
|
||||
throw_if_failed(close_dialog(luma::dialog_result::CANCELLED));
|
||||
return true;
|
||||
}
|
||||
if (dialog_->body && any_point_inside(event, dialog_body_bounds(bounds()))) {
|
||||
static_cast<void>(dialog_->body->handle_touch(event));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return content_->handle_touch(event);
|
||||
}
|
||||
|
||||
auto luma::overlay_host::invalidate_layout() -> void {
|
||||
assert(content_ != nullptr);
|
||||
begin_traversal();
|
||||
auto guard = scope_exit{[this] {
|
||||
end_traversal();
|
||||
}};
|
||||
content_->invalidate_layout();
|
||||
if (dialog_ && dialog_->body) {
|
||||
dialog_->body->invalidate_layout();
|
||||
}
|
||||
}
|
||||
|
||||
auto luma::overlay_host::bind_layout_invalidator(std::function<void()> invalidator) -> void {
|
||||
luma::control::bind_layout_invalidator(invalidator);
|
||||
assert(content_ != nullptr);
|
||||
content_->bind_layout_invalidator(invalidator);
|
||||
if (dialog_ && dialog_->body) {
|
||||
dialog_->body->bind_layout_invalidator(std::move(invalidator));
|
||||
}
|
||||
}
|
||||
|
||||
auto luma::overlays(std::unique_ptr<luma::control> content) -> std::expected<std::unique_ptr<luma::overlay_host>, modulus::error> {
|
||||
if (content == nullptr) {
|
||||
return invalid_value<std::unique_ptr<luma::overlay_host>>("overlay content must not be null");
|
||||
}
|
||||
return modulus::success(std::unique_ptr<luma::overlay_host>{
|
||||
new luma::overlay_host{std::move(content)},
|
||||
});
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
#include <luma/routing.hpp>
|
||||
|
||||
#include <cassert>
|
||||
#include <expected>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <modulus.hpp>
|
||||
#include <modulus/log.hpp>
|
||||
|
||||
#include <luma/detail/errors.hpp>
|
||||
|
||||
auto luma::router::register_route(std::string name, std::function<std::expected<std::unique_ptr<luma::control>, modulus::error>()> factory) -> std::expected<void, modulus::error> {
|
||||
if (name.empty()) {
|
||||
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "route name must not be empty");
|
||||
}
|
||||
if (!factory) {
|
||||
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "route factory must not be empty");
|
||||
}
|
||||
routes_[std::move(name)] = std::move(factory);
|
||||
return {};
|
||||
}
|
||||
|
||||
auto luma::router::set_retire_handler(std::function<void(std::unique_ptr<luma::control>)> handler) -> void {
|
||||
retire_handler_ = std::move(handler);
|
||||
}
|
||||
|
||||
auto luma::router::replace(std::string const& name) -> std::expected<void, modulus::error> {
|
||||
if (name.empty()) {
|
||||
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "route name must not be empty");
|
||||
}
|
||||
auto found = routes_.find(name);
|
||||
if (found == routes_.end()) {
|
||||
return luma::detail::fail<void>(luma::detail::error_not_found, "route not found: " + name);
|
||||
}
|
||||
assert(found->second);
|
||||
auto view = found->second();
|
||||
if (!view) {
|
||||
return modulus::fail<void>(view.error());
|
||||
}
|
||||
if (*view == nullptr) {
|
||||
return luma::detail::fail<void>(luma::detail::error_internal, "route factory returned null view: " + name);
|
||||
}
|
||||
retire_stack();
|
||||
stack_.push_back({
|
||||
.name = name,
|
||||
.view = std::move(*view),
|
||||
});
|
||||
modulus::log_debug("luma route replaced: " + name);
|
||||
return {};
|
||||
}
|
||||
|
||||
auto luma::router::push(std::string const& name) -> std::expected<void, modulus::error> {
|
||||
if (name.empty()) {
|
||||
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "route name must not be empty");
|
||||
}
|
||||
auto found = routes_.find(name);
|
||||
if (found == routes_.end()) {
|
||||
return luma::detail::fail<void>(luma::detail::error_not_found, "route not found: " + name);
|
||||
}
|
||||
assert(found->second);
|
||||
auto view = found->second();
|
||||
if (!view) {
|
||||
return modulus::fail<void>(view.error());
|
||||
}
|
||||
if (*view == nullptr) {
|
||||
return luma::detail::fail<void>(luma::detail::error_internal, "route factory returned null view: " + name);
|
||||
}
|
||||
stack_.push_back({
|
||||
.name = name,
|
||||
.view = std::move(*view),
|
||||
});
|
||||
modulus::log_debug("luma route pushed: " + name);
|
||||
return {};
|
||||
}
|
||||
|
||||
auto luma::router::pop() -> std::expected<void, modulus::error> {
|
||||
if (stack_.size() <= 1) {
|
||||
return luma::detail::fail<void>(luma::detail::error_unavailable, "cannot pop the last route");
|
||||
}
|
||||
auto view = std::move(stack_.back().view);
|
||||
auto name = stack_.back().name;
|
||||
stack_.pop_back();
|
||||
retire(std::move(view));
|
||||
modulus::log_debug("luma route popped: " + name);
|
||||
return {};
|
||||
}
|
||||
|
||||
auto luma::router::clear_stack() -> void {
|
||||
retire_stack();
|
||||
}
|
||||
|
||||
auto luma::router::current() -> luma::control* {
|
||||
if (stack_.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
assert(stack_.back().view != nullptr);
|
||||
return stack_.back().view.get();
|
||||
}
|
||||
|
||||
auto luma::router::current() const -> luma::control const* {
|
||||
if (stack_.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
assert(stack_.back().view != nullptr);
|
||||
return stack_.back().view.get();
|
||||
}
|
||||
|
||||
auto luma::router::current_route() const -> std::string const& {
|
||||
if (stack_.empty()) {
|
||||
return empty_route_;
|
||||
}
|
||||
return stack_.back().name;
|
||||
}
|
||||
|
||||
auto luma::router::retire(std::unique_ptr<luma::control> view) -> void {
|
||||
if (view == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (retire_handler_) {
|
||||
retire_handler_(std::move(view));
|
||||
}
|
||||
}
|
||||
|
||||
auto luma::router::retire_stack() -> void {
|
||||
auto removed = std::vector<luma::router::entry>{};
|
||||
removed.swap(stack_);
|
||||
for (auto& entry : removed) {
|
||||
retire(std::move(entry.view));
|
||||
}
|
||||
}
|
||||
+352
@@ -0,0 +1,352 @@
|
||||
#include <luma/theme.hpp>
|
||||
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
|
||||
#include <modulus.hpp>
|
||||
|
||||
#include <luma/detail/errors.hpp>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto json_options = modulus::json_config::options{
|
||||
.category = "luma",
|
||||
.invalid_argument_code = luma::detail::error_invalid_argument,
|
||||
.not_found_code = luma::detail::error_not_found
|
||||
};
|
||||
|
||||
auto invalid_color(std::string message) -> std::expected<luma::color, modulus::error> {
|
||||
assert(!message.empty());
|
||||
return luma::detail::fail<luma::color>(luma::detail::error_invalid_argument, std::move(message));
|
||||
}
|
||||
|
||||
auto invalid_theme(std::string message) -> std::expected<luma::theme, modulus::error> {
|
||||
assert(!message.empty());
|
||||
return luma::detail::fail<luma::theme>(luma::detail::error_invalid_argument, std::move(message));
|
||||
}
|
||||
|
||||
auto invalid_status(std::string message) -> std::expected<void, modulus::error> {
|
||||
assert(!message.empty());
|
||||
return luma::detail::fail<void>(luma::detail::error_invalid_argument, std::move(message));
|
||||
}
|
||||
|
||||
auto byte_value(boost::json::value const& value, std::char_t const* name) -> std::expected<std::uint8_t, modulus::error> {
|
||||
assert(name != nullptr);
|
||||
auto const* integer = value.if_int64();
|
||||
if (integer == nullptr || *integer < 0 || *integer > 255) {
|
||||
return luma::detail::fail<std::uint8_t>(luma::detail::error_invalid_argument, std::string{name} + " must be an integer from 0 to 255");
|
||||
}
|
||||
return modulus::success(static_cast<std::uint8_t>(*integer));
|
||||
}
|
||||
|
||||
auto apply_color(boost::json::object const& object, std::char_t const* name, luma::color& target) -> std::expected<void, modulus::error> {
|
||||
assert(name != nullptr);
|
||||
if (auto const* value = object.if_contains(name)) {
|
||||
auto parsed = luma::parse_color(*value);
|
||||
if (!parsed) {
|
||||
return modulus::fail<void>(parsed.error());
|
||||
}
|
||||
target = *parsed;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto number_value(boost::json::value const& value) -> std::optional<std::float64_t>;
|
||||
|
||||
auto apply_text_style(boost::json::object const& object, std::char_t const* name, luma::text_style& target) -> std::expected<void, modulus::error> {
|
||||
assert(name != nullptr);
|
||||
if (auto const* value = object.if_contains(name)) {
|
||||
auto style = modulus::json_config::require_object(*value, name, json_options);
|
||||
if (!style) {
|
||||
return modulus::fail<void>(style.error());
|
||||
}
|
||||
auto unknown = modulus::json_config::reject_unknown_fields(**style, {
|
||||
"font",
|
||||
"size",
|
||||
"line_height",
|
||||
}, name, json_options);
|
||||
if (!unknown) {
|
||||
return modulus::fail<void>(unknown.error());
|
||||
}
|
||||
if (auto const* font = (*style)->if_contains("font")) {
|
||||
auto const* text = font->if_string();
|
||||
if (text == nullptr) {
|
||||
return luma::detail::fail<void>(luma::detail::error_invalid_argument, std::string{name} + ".font must be a string");
|
||||
}
|
||||
target.font = std::string{text->c_str()};
|
||||
}
|
||||
if (auto const* size = (*style)->if_contains("size")) {
|
||||
auto number = number_value(*size);
|
||||
if (!number || *number <= 0.0) {
|
||||
return luma::detail::fail<void>(luma::detail::error_invalid_argument, std::string{name} + ".size must be positive");
|
||||
}
|
||||
target.size = static_cast<std::float32_t>(*number);
|
||||
}
|
||||
if (auto const* line_height = (*style)->if_contains("line_height")) {
|
||||
auto number = number_value(*line_height);
|
||||
if (!number || *number <= 0.0) {
|
||||
return luma::detail::fail<void>(luma::detail::error_invalid_argument, std::string{name} + ".line_height must be positive");
|
||||
}
|
||||
target.line_height = static_cast<std::float32_t>(*number);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto number_value(boost::json::value const& value) -> std::optional<std::float64_t> {
|
||||
if (auto const* number = value.if_double()) {
|
||||
return *number;
|
||||
}
|
||||
if (auto const* integer = value.if_int64()) {
|
||||
return static_cast<std::float64_t>(*integer);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
auto luma::default_theme() -> luma::theme {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto luma::validate(luma::text_style const& style) -> std::expected<void, modulus::error> {
|
||||
if (style.font.empty()) {
|
||||
return invalid_status("text style font must not be empty");
|
||||
}
|
||||
if (!std::isfinite(style.size) || style.size <= 0.0F) {
|
||||
return invalid_status("text style size must be finite and positive");
|
||||
}
|
||||
if (!std::isfinite(style.line_height) || style.line_height <= 0.0F) {
|
||||
return invalid_status("text style line height must be finite and positive");
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto luma::validate(luma::theme const& theme) -> std::expected<void, modulus::error> {
|
||||
if (auto status = luma::validate(theme.body); !status) {
|
||||
return modulus::fail<void>(status.error());
|
||||
}
|
||||
if (auto status = luma::validate(theme.title); !status) {
|
||||
return modulus::fail<void>(status.error());
|
||||
}
|
||||
if (!std::isfinite(theme.radius) || theme.radius < 0.0F) {
|
||||
return invalid_status("theme.radius must be finite and non-negative");
|
||||
}
|
||||
if (!std::isfinite(theme.spacing) || theme.spacing < 0.0F) {
|
||||
return invalid_status("theme.spacing must be finite and non-negative");
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto luma::parse_color(boost::json::value const& value) -> std::expected<luma::color, modulus::error> {
|
||||
auto object = modulus::json_config::require_object(value, "color", json_options);
|
||||
if (!object) {
|
||||
return invalid_color(std::string{object.error().message()});
|
||||
}
|
||||
auto unknown = modulus::json_config::reject_unknown_fields(**object, {
|
||||
"r",
|
||||
"g",
|
||||
"b",
|
||||
"a",
|
||||
}, "color", json_options);
|
||||
if (!unknown) {
|
||||
return invalid_color(std::string{unknown.error().message()});
|
||||
}
|
||||
auto const* red_value = (*object)->if_contains("r");
|
||||
auto const* green_value = (*object)->if_contains("g");
|
||||
auto const* blue_value = (*object)->if_contains("b");
|
||||
if (red_value == nullptr || green_value == nullptr || blue_value == nullptr) {
|
||||
return invalid_color("color requires r, g, and b fields");
|
||||
}
|
||||
auto red = byte_value(*red_value, "r");
|
||||
auto green = byte_value(*green_value, "g");
|
||||
auto blue = byte_value(*blue_value, "b");
|
||||
if (!red) {
|
||||
return invalid_color(std::string{red.error().message()});
|
||||
}
|
||||
if (!green) {
|
||||
return invalid_color(std::string{green.error().message()});
|
||||
}
|
||||
if (!blue) {
|
||||
return invalid_color(std::string{blue.error().message()});
|
||||
}
|
||||
auto alpha = std::uint8_t{255};
|
||||
if (auto const* alpha_value = (*object)->if_contains("a")) {
|
||||
auto parsed = byte_value(*alpha_value, "a");
|
||||
if (!parsed) {
|
||||
return invalid_color(std::string{parsed.error().message()});
|
||||
}
|
||||
alpha = *parsed;
|
||||
}
|
||||
return modulus::success(luma::color{
|
||||
.r = *red,
|
||||
.g = *green,
|
||||
.b = *blue,
|
||||
.a = alpha,
|
||||
});
|
||||
}
|
||||
|
||||
auto luma::parse_theme(boost::json::value const& value) -> std::expected<luma::theme, modulus::error> {
|
||||
auto object = modulus::json_config::require_object(value, "theme", json_options);
|
||||
if (!object) {
|
||||
return invalid_theme(std::string{object.error().message()});
|
||||
}
|
||||
auto unknown = modulus::json_config::reject_unknown_fields(**object, {
|
||||
"background",
|
||||
"surface",
|
||||
"surface_active",
|
||||
"text",
|
||||
"text_muted",
|
||||
"border",
|
||||
"accent",
|
||||
"body",
|
||||
"title",
|
||||
"radius",
|
||||
"spacing",
|
||||
}, "theme", json_options);
|
||||
if (!unknown) {
|
||||
return invalid_theme(std::string{unknown.error().message()});
|
||||
}
|
||||
auto parsed = luma::default_theme();
|
||||
if (auto status = apply_color(**object, "background", parsed.background); !status) {
|
||||
return invalid_theme(std::string{status.error().message()});
|
||||
}
|
||||
if (auto status = apply_color(**object, "surface", parsed.surface); !status) {
|
||||
return invalid_theme(std::string{status.error().message()});
|
||||
}
|
||||
if (auto status = apply_color(**object, "surface_active", parsed.surface_active); !status) {
|
||||
return invalid_theme(std::string{status.error().message()});
|
||||
}
|
||||
if (auto status = apply_color(**object, "text", parsed.text); !status) {
|
||||
return invalid_theme(std::string{status.error().message()});
|
||||
}
|
||||
if (auto status = apply_color(**object, "text_muted", parsed.text_muted); !status) {
|
||||
return invalid_theme(std::string{status.error().message()});
|
||||
}
|
||||
if (auto status = apply_color(**object, "border", parsed.border); !status) {
|
||||
return invalid_theme(std::string{status.error().message()});
|
||||
}
|
||||
if (auto status = apply_color(**object, "accent", parsed.accent); !status) {
|
||||
return invalid_theme(std::string{status.error().message()});
|
||||
}
|
||||
if (auto status = apply_text_style(**object, "body", parsed.body); !status) {
|
||||
return invalid_theme(std::string{status.error().message()});
|
||||
}
|
||||
if (auto status = apply_text_style(**object, "title", parsed.title); !status) {
|
||||
return invalid_theme(std::string{status.error().message()});
|
||||
}
|
||||
if (auto const* radius = (*object)->if_contains("radius")) {
|
||||
auto number = number_value(*radius);
|
||||
if (!number || *number < 0.0) {
|
||||
return invalid_theme("theme.radius must be non-negative");
|
||||
}
|
||||
parsed.radius = static_cast<std::float32_t>(*number);
|
||||
}
|
||||
if (auto const* spacing = (*object)->if_contains("spacing")) {
|
||||
auto number = number_value(*spacing);
|
||||
if (!number || *number < 0.0) {
|
||||
return invalid_theme("theme.spacing must be non-negative");
|
||||
}
|
||||
parsed.spacing = static_cast<std::float32_t>(*number);
|
||||
}
|
||||
if (auto status = luma::validate(parsed); !status) {
|
||||
return invalid_theme(std::string{status.error().message()});
|
||||
}
|
||||
return modulus::success(parsed);
|
||||
}
|
||||
|
||||
auto luma::to_json(luma::color const& color) -> boost::json::value {
|
||||
return boost::json::object{
|
||||
{
|
||||
"r",
|
||||
static_cast<std::int64_t>(color.r),
|
||||
},
|
||||
{
|
||||
"g",
|
||||
static_cast<std::int64_t>(color.g),
|
||||
},
|
||||
{
|
||||
"b",
|
||||
static_cast<std::int64_t>(color.b),
|
||||
},
|
||||
{
|
||||
"a",
|
||||
static_cast<std::int64_t>(color.a),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
auto luma::to_json(luma::text_style const& style) -> boost::json::value {
|
||||
return boost::json::object{
|
||||
{
|
||||
"font",
|
||||
style.font,
|
||||
},
|
||||
{
|
||||
"size",
|
||||
static_cast<std::float64_t>(style.size),
|
||||
},
|
||||
{
|
||||
"line_height",
|
||||
static_cast<std::float64_t>(style.line_height),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
auto luma::to_json(luma::theme const& theme) -> boost::json::value {
|
||||
return boost::json::object{
|
||||
{
|
||||
"background",
|
||||
luma::to_json(theme.background),
|
||||
},
|
||||
{
|
||||
"surface",
|
||||
luma::to_json(theme.surface),
|
||||
},
|
||||
{
|
||||
"surface_active",
|
||||
luma::to_json(theme.surface_active),
|
||||
},
|
||||
{
|
||||
"text",
|
||||
luma::to_json(theme.text),
|
||||
},
|
||||
{
|
||||
"text_muted",
|
||||
luma::to_json(theme.text_muted),
|
||||
},
|
||||
{
|
||||
"border",
|
||||
luma::to_json(theme.border),
|
||||
},
|
||||
{
|
||||
"accent",
|
||||
luma::to_json(theme.accent),
|
||||
},
|
||||
{
|
||||
"body",
|
||||
luma::to_json(theme.body),
|
||||
},
|
||||
{
|
||||
"title",
|
||||
luma::to_json(theme.title),
|
||||
},
|
||||
{
|
||||
"radius",
|
||||
static_cast<std::float64_t>(theme.radius),
|
||||
},
|
||||
{
|
||||
"spacing",
|
||||
static_cast<std::float64_t>(theme.spacing),
|
||||
},
|
||||
};
|
||||
}
|
||||
+540
@@ -0,0 +1,540 @@
|
||||
#include <luma/window.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <modulus.hpp>
|
||||
#include <modulus/log.hpp>
|
||||
|
||||
#include <luma/detail/errors.hpp>
|
||||
#include <luma/geometry.hpp>
|
||||
#include <luma/input.hpp>
|
||||
#include <luma/render.hpp>
|
||||
#include <luma/theme.hpp>
|
||||
|
||||
namespace {
|
||||
|
||||
class scope_exit {
|
||||
public:
|
||||
explicit scope_exit(std::function<void()> action);
|
||||
scope_exit(scope_exit const&) = delete;
|
||||
auto operator=(scope_exit const&) -> scope_exit& = delete;
|
||||
~scope_exit();
|
||||
private:
|
||||
std::function<void()> action_{};
|
||||
};
|
||||
|
||||
constexpr auto window_title_vertical_padding = std::float32_t{6.0F};
|
||||
|
||||
auto finite_rect(luma::rect value) -> std::bool_t {
|
||||
return std::isfinite(value.x) && std::isfinite(value.y) && std::isfinite(value.width) && std::isfinite(value.height) && value.width >= 0.0F && value.height >= 0.0F;
|
||||
}
|
||||
|
||||
auto title_height(luma::window_options const& options, std::float32_t line_height) -> std::float32_t {
|
||||
if (!options.decorated) {
|
||||
return {};
|
||||
}
|
||||
auto desired = line_height + window_title_vertical_padding * 2.0F;
|
||||
return std::min(desired, options.bounds.height);
|
||||
}
|
||||
|
||||
auto title_bounds(luma::window_options const& options, std::float32_t line_height) -> luma::rect {
|
||||
return {
|
||||
.x = options.bounds.x,
|
||||
.y = options.bounds.y,
|
||||
.width = options.bounds.width,
|
||||
.height = title_height(options, line_height),
|
||||
};
|
||||
}
|
||||
|
||||
auto client_bounds(luma::window_options const& options, std::float32_t line_height) -> luma::rect {
|
||||
if (!options.decorated) {
|
||||
return options.bounds;
|
||||
}
|
||||
auto title = title_bounds(options, line_height);
|
||||
return {
|
||||
.x = options.bounds.x,
|
||||
.y = options.bounds.y + title.height,
|
||||
.width = options.bounds.width,
|
||||
.height = std::max(std::float32_t{}, options.bounds.height - title.height),
|
||||
};
|
||||
}
|
||||
|
||||
template <typename VALUE>
|
||||
auto invalid(std::string message) -> std::expected<VALUE, modulus::error> {
|
||||
return luma::detail::fail<VALUE>(luma::detail::error_invalid_argument, std::move(message));
|
||||
}
|
||||
|
||||
template <typename VALUE>
|
||||
auto unavailable(std::string message) -> std::expected<VALUE, modulus::error> {
|
||||
return luma::detail::fail<VALUE>(luma::detail::error_unavailable, std::move(message));
|
||||
}
|
||||
|
||||
auto make_event(luma::touch_event const& event, luma::touch_point point) -> luma::touch_event {
|
||||
return {
|
||||
.points = {point},
|
||||
.time = event.time,
|
||||
};
|
||||
}
|
||||
|
||||
scope_exit::scope_exit(std::function<void()> action) : action_{std::move(action)} {}
|
||||
|
||||
scope_exit::~scope_exit() {
|
||||
if (action_) {
|
||||
action_();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
luma::window::window(std::uint64_t id, luma::window_options options, std::unique_ptr<luma::control> content) : id_{id}, options_{std::move(options)}, content_{std::move(content)} {
|
||||
assert(id_ != 0);
|
||||
assert(finite_rect(options_.bounds));
|
||||
assert(content_ != nullptr);
|
||||
}
|
||||
|
||||
auto luma::window::id() const -> std::uint64_t {
|
||||
assert(id_ != 0);
|
||||
return id_;
|
||||
}
|
||||
|
||||
auto luma::window::options() const -> luma::window_options const& {
|
||||
assert(finite_rect(options_.bounds));
|
||||
return options_;
|
||||
}
|
||||
|
||||
auto luma::window::set_bounds(luma::rect bounds) -> std::expected<void, modulus::error> {
|
||||
if (!finite_rect(bounds)) {
|
||||
return invalid<void>("window bounds must be finite and non-negative");
|
||||
}
|
||||
options_.bounds = bounds;
|
||||
return {};
|
||||
}
|
||||
|
||||
auto luma::window::set_visible(std::bool_t value) -> void {
|
||||
options_.visible = value;
|
||||
}
|
||||
|
||||
auto luma::window::visible() const -> std::bool_t {
|
||||
return options_.visible;
|
||||
}
|
||||
|
||||
auto luma::window::content() -> luma::control* {
|
||||
assert(content_ != nullptr);
|
||||
return content_.get();
|
||||
}
|
||||
|
||||
auto luma::window::content() const -> luma::control const* {
|
||||
assert(content_ != nullptr);
|
||||
return content_.get();
|
||||
}
|
||||
|
||||
auto luma::window_manager::open(luma::window_options options, std::unique_ptr<luma::control> content) -> std::expected<std::uint64_t, modulus::error> {
|
||||
if (next_id_ == 0) {
|
||||
return unavailable<std::uint64_t>("window id space exhausted");
|
||||
}
|
||||
if (!finite_rect(options.bounds)) {
|
||||
return invalid<std::uint64_t>("window bounds must be finite and non-negative");
|
||||
}
|
||||
if (content == nullptr) {
|
||||
return invalid<std::uint64_t>("window content must not be null");
|
||||
}
|
||||
content->bind_layout_invalidator(layout_invalidator());
|
||||
auto id = next_id_++;
|
||||
auto created = std::unique_ptr<luma::window>{
|
||||
new luma::window{
|
||||
id,
|
||||
std::move(options),
|
||||
std::move(content),
|
||||
},
|
||||
};
|
||||
if (mutation_depth_ > 0) {
|
||||
pending_operations_.push_back({
|
||||
.kind = luma::window_manager::pending_kind::OPEN,
|
||||
.id = id,
|
||||
.window = std::move(created),
|
||||
});
|
||||
} else {
|
||||
windows_.push_back(std::move(created));
|
||||
}
|
||||
modulus::log_debug("luma window opened: " + std::to_string(id));
|
||||
return modulus::success(id);
|
||||
}
|
||||
|
||||
auto luma::window_manager::close(std::uint64_t id) -> std::expected<void, modulus::error> {
|
||||
if (id == 0) {
|
||||
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "window id must not be zero");
|
||||
}
|
||||
if (!contains_effective(id)) {
|
||||
return luma::detail::fail<void>(luma::detail::error_not_found, "window not found");
|
||||
}
|
||||
if (mutation_depth_ > 0) {
|
||||
pending_operations_.push_back({
|
||||
.kind = luma::window_manager::pending_kind::CLOSE,
|
||||
.id = id,
|
||||
});
|
||||
} else {
|
||||
close_now(id);
|
||||
}
|
||||
modulus::log_debug("luma window close requested: " + std::to_string(id));
|
||||
return {};
|
||||
}
|
||||
|
||||
auto luma::window_manager::bring_to_front(std::uint64_t id) -> std::expected<void, modulus::error> {
|
||||
if (id == 0) {
|
||||
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "window id must not be zero");
|
||||
}
|
||||
if (!contains_effective(id)) {
|
||||
return luma::detail::fail<void>(luma::detail::error_not_found, "window not found");
|
||||
}
|
||||
if (mutation_depth_ > 0) {
|
||||
pending_operations_.push_back({
|
||||
.kind = luma::window_manager::pending_kind::BRING_TO_FRONT,
|
||||
.id = id,
|
||||
});
|
||||
} else {
|
||||
bring_to_front_now(id);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto luma::window_manager::set_bounds(std::uint64_t id, luma::rect bounds) -> std::expected<void, modulus::error> {
|
||||
if (id == 0) {
|
||||
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "window id must not be zero");
|
||||
}
|
||||
if (!finite_rect(bounds)) {
|
||||
return invalid<void>("window bounds must be finite and non-negative");
|
||||
}
|
||||
if (!contains_effective(id)) {
|
||||
return luma::detail::fail<void>(luma::detail::error_not_found, "window not found");
|
||||
}
|
||||
if (mutation_depth_ > 0) {
|
||||
pending_operations_.push_back({
|
||||
.kind = luma::window_manager::pending_kind::SET_BOUNDS,
|
||||
.id = id,
|
||||
.bounds = bounds,
|
||||
});
|
||||
} else {
|
||||
set_bounds_now(id, bounds);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto luma::window_manager::set_visible(std::uint64_t id, std::bool_t visible) -> std::expected<void, modulus::error> {
|
||||
if (id == 0) {
|
||||
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "window id must not be zero");
|
||||
}
|
||||
if (!contains_effective(id)) {
|
||||
return luma::detail::fail<void>(luma::detail::error_not_found, "window not found");
|
||||
}
|
||||
if (mutation_depth_ > 0) {
|
||||
pending_operations_.push_back({
|
||||
.kind = luma::window_manager::pending_kind::SET_VISIBLE,
|
||||
.id = id,
|
||||
.visible = visible,
|
||||
});
|
||||
} else {
|
||||
set_visible_now(id, visible);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto luma::window_manager::find(std::uint64_t id) -> luma::window* {
|
||||
if (id == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
auto found = std::find_if(windows_.begin(), windows_.end(), [id](auto const& window) { return window && window->id() == id; });
|
||||
return found == windows_.end() ? nullptr : found->get();
|
||||
}
|
||||
|
||||
auto luma::window_manager::clear() -> void {
|
||||
if (mutation_depth_ > 0) {
|
||||
pending_operations_.push_back({
|
||||
.kind = luma::window_manager::pending_kind::CLEAR,
|
||||
});
|
||||
return;
|
||||
}
|
||||
windows_.clear();
|
||||
active_touches_.clear();
|
||||
}
|
||||
|
||||
auto luma::window_manager::measure(luma::draw_context& context, luma::theme const& theme, luma::constraints limits) -> luma::size {
|
||||
title_line_height_ = theme.body.line_height;
|
||||
++mutation_depth_;
|
||||
auto guard = scope_exit{[this] {
|
||||
assert(mutation_depth_ > 0);
|
||||
--mutation_depth_;
|
||||
apply_pending_operations();
|
||||
}};
|
||||
for (auto& window : windows_) {
|
||||
assert(window != nullptr);
|
||||
if (!window->visible()) {
|
||||
continue;
|
||||
}
|
||||
assert(window->content() != nullptr);
|
||||
auto client = client_bounds(window->options(), title_line_height_);
|
||||
window->content()->measure(context, theme, {
|
||||
.min = {},
|
||||
.max = {
|
||||
.width = client.width,
|
||||
.height = client.height,
|
||||
},
|
||||
});
|
||||
}
|
||||
return luma::clamp(limits.max, limits);
|
||||
}
|
||||
|
||||
auto luma::window_manager::arrange(luma::rect bounds) -> void {
|
||||
assert(finite_rect(bounds));
|
||||
++mutation_depth_;
|
||||
auto guard = scope_exit{[this] {
|
||||
assert(mutation_depth_ > 0);
|
||||
--mutation_depth_;
|
||||
apply_pending_operations();
|
||||
}};
|
||||
luma::control::arrange(bounds);
|
||||
for (auto& window : windows_) {
|
||||
assert(window != nullptr);
|
||||
if (window->visible()) {
|
||||
assert(window->content() != nullptr);
|
||||
window->content()->arrange(client_bounds(window->options(), title_line_height_));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto luma::window_manager::draw(luma::draw_context& context, luma::theme const& theme) const -> void {
|
||||
title_line_height_ = theme.body.line_height;
|
||||
++mutation_depth_;
|
||||
auto guard = scope_exit{[this] {
|
||||
assert(mutation_depth_ > 0);
|
||||
--mutation_depth_;
|
||||
apply_pending_operations();
|
||||
}};
|
||||
for (auto const& window : windows_) {
|
||||
assert(window != nullptr);
|
||||
if (!window->visible()) {
|
||||
continue;
|
||||
}
|
||||
auto bounds = window->options().bounds;
|
||||
auto client = client_bounds(window->options(), title_line_height_);
|
||||
context.fill_rect(bounds, theme.surface);
|
||||
assert(window->content() != nullptr);
|
||||
context.push_clip(client);
|
||||
window->content()->draw(context, theme);
|
||||
context.pop_clip();
|
||||
if (window->options().decorated) {
|
||||
auto title = title_bounds(window->options(), title_line_height_);
|
||||
context.fill_rect(title, theme.surface_active);
|
||||
context.draw_text(window->options().title, {
|
||||
.x = title.x + 8.0F,
|
||||
.y = title.y + window_title_vertical_padding,
|
||||
}, theme.body, theme.text);
|
||||
}
|
||||
context.stroke_rect(bounds, 1.0F, theme.border);
|
||||
}
|
||||
}
|
||||
|
||||
auto luma::window_manager::handle_touch(luma::touch_event const& event) -> std::bool_t {
|
||||
if (event.points.empty()) {
|
||||
return false;
|
||||
}
|
||||
++mutation_depth_;
|
||||
auto guard = scope_exit{[this] {
|
||||
assert(mutation_depth_ > 0);
|
||||
--mutation_depth_;
|
||||
apply_pending_operations();
|
||||
}};
|
||||
auto handled = false;
|
||||
for (auto const& point : event.points) {
|
||||
if (auto captured = active_touches_.find(point.id); captured != active_touches_.end()) {
|
||||
auto const window_id = captured->second;
|
||||
auto* captured_window = find(window_id);
|
||||
auto const captured_visible = captured_window != nullptr && captured_window->visible();
|
||||
if (captured_visible) {
|
||||
assert(captured_window->content() != nullptr);
|
||||
auto single = make_event(event, point);
|
||||
static_cast<void>(captured_window->content()->handle_touch(single));
|
||||
handled = true;
|
||||
}
|
||||
if (point.phase == luma::touch_phase::UP || point.phase == luma::touch_phase::CANCEL || !captured_visible) {
|
||||
active_touches_.erase(point.id);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
for (auto current = windows_.rbegin(); current != windows_.rend(); ++current) {
|
||||
assert(*current != nullptr);
|
||||
if (!(*current)->visible()) {
|
||||
continue;
|
||||
}
|
||||
if (luma::contains((*current)->options().bounds, point.position)) {
|
||||
auto consumed = false;
|
||||
if (luma::contains(client_bounds((*current)->options(), title_line_height_), point.position)) {
|
||||
assert((*current)->content() != nullptr);
|
||||
auto single = make_event(event, point);
|
||||
consumed = (*current)->content()->handle_touch(single);
|
||||
if (point.phase == luma::touch_phase::DOWN && (consumed || (*current)->options().modal)) {
|
||||
active_touches_[point.id] = (*current)->id();
|
||||
}
|
||||
}
|
||||
handled = true;
|
||||
break;
|
||||
}
|
||||
if ((*current)->options().modal) {
|
||||
handled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return handled;
|
||||
}
|
||||
|
||||
auto luma::window_manager::contains_effective(std::uint64_t id) const -> std::bool_t {
|
||||
if (id == 0) {
|
||||
return false;
|
||||
}
|
||||
auto exists = std::find_if(windows_.begin(), windows_.end(), [id](auto const& window) { return window && window->id() == id; }) != windows_.end();
|
||||
for (auto const& operation : pending_operations_) {
|
||||
switch (operation.kind) {
|
||||
case luma::window_manager::pending_kind::OPEN:
|
||||
if (operation.id == id) {
|
||||
exists = true;
|
||||
}
|
||||
break;
|
||||
case luma::window_manager::pending_kind::CLOSE:
|
||||
if (operation.id == id) {
|
||||
exists = false;
|
||||
}
|
||||
break;
|
||||
case luma::window_manager::pending_kind::BRING_TO_FRONT:
|
||||
case luma::window_manager::pending_kind::SET_BOUNDS:
|
||||
case luma::window_manager::pending_kind::SET_VISIBLE:
|
||||
break;
|
||||
case luma::window_manager::pending_kind::CLEAR:
|
||||
exists = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return exists;
|
||||
}
|
||||
|
||||
auto luma::window_manager::close_now(std::uint64_t id) const -> void {
|
||||
auto found = std::find_if(windows_.begin(), windows_.end(), [id](auto const& window) { return window && window->id() == id; });
|
||||
if (found == windows_.end()) {
|
||||
return;
|
||||
}
|
||||
auto const closed_id = (*found)->id();
|
||||
for (auto current = active_touches_.begin(); current != active_touches_.end();) {
|
||||
if (current->second == closed_id) {
|
||||
current = active_touches_.erase(current);
|
||||
} else {
|
||||
++current;
|
||||
}
|
||||
}
|
||||
windows_.erase(found);
|
||||
}
|
||||
|
||||
auto luma::window_manager::bring_to_front_now(std::uint64_t id) const -> void {
|
||||
auto found = std::find_if(windows_.begin(), windows_.end(), [id](auto const& window) { return window && window->id() == id; });
|
||||
if (found == windows_.end()) {
|
||||
return;
|
||||
}
|
||||
auto moved = std::move(*found);
|
||||
windows_.erase(found);
|
||||
windows_.push_back(std::move(moved));
|
||||
}
|
||||
|
||||
auto luma::window_manager::set_bounds_now(std::uint64_t id, luma::rect bounds) const -> void {
|
||||
auto* window = const_cast<luma::window_manager*>(this)->find(id);
|
||||
if (window != nullptr) {
|
||||
static_cast<void>(window->set_bounds(bounds));
|
||||
}
|
||||
}
|
||||
|
||||
auto luma::window_manager::set_visible_now(std::uint64_t id, std::bool_t visible) const -> void {
|
||||
auto* window = const_cast<luma::window_manager*>(this)->find(id);
|
||||
if (window != nullptr) {
|
||||
window->set_visible(visible);
|
||||
}
|
||||
}
|
||||
|
||||
auto luma::window_manager::apply_pending_operations() const -> void {
|
||||
if (mutation_depth_ != 0 || pending_operations_.empty()) {
|
||||
return;
|
||||
}
|
||||
auto pending = std::vector<luma::window_manager::pending_operation>{};
|
||||
pending.swap(pending_operations_);
|
||||
for (auto& operation : pending) {
|
||||
switch (operation.kind) {
|
||||
case luma::window_manager::pending_kind::OPEN:
|
||||
if (operation.window) {
|
||||
windows_.push_back(std::move(operation.window));
|
||||
}
|
||||
break;
|
||||
case luma::window_manager::pending_kind::CLOSE:
|
||||
close_now(operation.id);
|
||||
break;
|
||||
case luma::window_manager::pending_kind::BRING_TO_FRONT:
|
||||
bring_to_front_now(operation.id);
|
||||
break;
|
||||
case luma::window_manager::pending_kind::SET_BOUNDS:
|
||||
set_bounds_now(operation.id, operation.bounds);
|
||||
break;
|
||||
case luma::window_manager::pending_kind::SET_VISIBLE:
|
||||
set_visible_now(operation.id, operation.visible);
|
||||
break;
|
||||
case luma::window_manager::pending_kind::CLEAR:
|
||||
windows_.clear();
|
||||
active_touches_.clear();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto luma::window_manager::invalidate_layout() -> void {
|
||||
++mutation_depth_;
|
||||
auto guard = scope_exit{[this] {
|
||||
assert(mutation_depth_ > 0);
|
||||
--mutation_depth_;
|
||||
apply_pending_operations();
|
||||
}};
|
||||
auto controls = std::vector<luma::control*>{};
|
||||
controls.reserve(windows_.size() + pending_operations_.size());
|
||||
for (auto& window : windows_) {
|
||||
assert(window != nullptr);
|
||||
assert(window->content() != nullptr);
|
||||
controls.push_back(window->content());
|
||||
}
|
||||
for (auto& operation : pending_operations_) {
|
||||
if (operation.window) {
|
||||
assert(operation.window->content() != nullptr);
|
||||
controls.push_back(operation.window->content());
|
||||
}
|
||||
}
|
||||
for (auto* control : controls) {
|
||||
assert(control != nullptr);
|
||||
control->invalidate_layout();
|
||||
}
|
||||
}
|
||||
|
||||
auto luma::window_manager::bind_layout_invalidator(std::function<void()> invalidator) -> void {
|
||||
luma::control::bind_layout_invalidator(invalidator);
|
||||
for (auto& window : windows_) {
|
||||
assert(window != nullptr);
|
||||
assert(window->content() != nullptr);
|
||||
window->content()->bind_layout_invalidator(invalidator);
|
||||
}
|
||||
for (auto& operation : pending_operations_) {
|
||||
if (operation.window) {
|
||||
assert(operation.window->content() != nullptr);
|
||||
operation.window->content()->bind_layout_invalidator(invalidator);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user