This commit is contained in:
+540
@@ -0,0 +1,540 @@
|
||||
#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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user