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

366 lines
11 KiB
C++

#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::CANCELED));
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)},
});
}