#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { class scope_exit { public: explicit scope_exit(std::function action); scope_exit(scope_exit const &) = delete; auto operator=(scope_exit const &) -> scope_exit & = delete; ~scope_exit(); private: std::function action_{}; }; auto invalid(std::string message) -> std::expected { return luma::detail::fail(luma::detail::error_invalid_argument, std::move(message)); } auto unavailable(std::string message) -> std::expected { return luma::detail::fail(luma::detail::error_unavailable, std::move(message)); } template auto invalid_value(std::string message) -> std::expected { return luma::detail::fail(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 const &status) -> void { if (!status) { throw std::runtime_error(status.error().describe()); } } scope_exit::scope_exit(std::function action) : action_{std::move(action)} {} scope_exit::~scope_exit() { if (action_) { action_(); } } } luma::overlay_host::overlay_host(std::unique_ptr content) : content_{std::move(content)} { assert(content_ != nullptr); } auto luma::overlay_host::show_dialog(luma::dialog_options options) -> std::expected { 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 { 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(luma::detail::error_internal, std::string{"dialog close callback failed: "} + error.what()); } catch (...) { return luma::detail::fail(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 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 { 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(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 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 content) -> std::expected, modulus::error> { if (content == nullptr) { return invalid_value>("overlay content must not be null"); } return modulus::success(std::unique_ptr{ new luma::overlay_host{std::move(content)}, }); }