366 lines
11 KiB
C++
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)},
|
|
});
|
|
}
|