Initial public khodo snapshot
publish / publish (push) Successful in 27m4s

This commit is contained in:
khodowany
2026-09-04 19:30:11 -05:00
commit 51339606a2
38 changed files with 8989 additions and 0 deletions
+193
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+126
View File
@@ -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
View File
@@ -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),
},
};
}
+77
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+10
View File
@@ -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);
}
+359
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
}
}