#include #include #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_{}; }; 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 auto invalid(std::string message) -> std::expected { return luma::detail::fail(luma::detail::error_invalid_argument, std::move(message)); } template auto unavailable(std::string message) -> std::expected { return luma::detail::fail(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 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 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 { if (!finite_rect(bounds)) { return invalid("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 content) -> std::expected { if (next_id_ == 0) { return unavailable("window id space exhausted"); } if (!finite_rect(options.bounds)) { return invalid("window bounds must be finite and non-negative"); } if (content == nullptr) { return invalid("window content must not be null"); } content->bind_layout_invalidator(layout_invalidator()); auto id = next_id_++; auto created = std::unique_ptr{ 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 { if (id == 0) { return luma::detail::fail(luma::detail::error_invalid_argument, "window id must not be zero"); } if (!contains_effective(id)) { return luma::detail::fail(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 { if (id == 0) { return luma::detail::fail(luma::detail::error_invalid_argument, "window id must not be zero"); } if (!contains_effective(id)) { return luma::detail::fail(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 { if (id == 0) { return luma::detail::fail(luma::detail::error_invalid_argument, "window id must not be zero"); } if (!finite_rect(bounds)) { return invalid("window bounds must be finite and non-negative"); } if (!contains_effective(id)) { return luma::detail::fail(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 { if (id == 0) { return luma::detail::fail(luma::detail::error_invalid_argument, "window id must not be zero"); } if (!contains_effective(id)) { return luma::detail::fail(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(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(this)->find(id); if (window != nullptr) { static_cast(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(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{}; 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{}; 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 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); } } }