Files
luma/src/window.cpp
khodowany bb96aaf95d
publish / publish (push) Successful in 15m37s
Formatting
2026-09-14 23:56:06 -05:00

544 lines
17 KiB
C++

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