+11
-467
@@ -1,4 +1,5 @@
|
||||
#include <luma/application.hpp>
|
||||
#include "detail/raylib_backend.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
@@ -73,68 +74,6 @@ struct application_core {
|
||||
|
||||
namespace {
|
||||
|
||||
struct font_key {
|
||||
std::string path{};
|
||||
std::int32_t size{};
|
||||
auto operator==(font_key const &other) const -> std::bool_t;
|
||||
};
|
||||
|
||||
struct font_key_hash {
|
||||
auto operator()(font_key const &key) const noexcept -> std::size_t;
|
||||
};
|
||||
|
||||
struct raylib_resource_state {
|
||||
std::thread::id ui_thread{};
|
||||
std::atomic_bool active{false};
|
||||
std::mutex mutex{};
|
||||
struct texture_record {
|
||||
Texture2D texture{};
|
||||
std::bool_t unloaded{false};
|
||||
};
|
||||
std::vector<std::weak_ptr<texture_record>> records{};
|
||||
std::vector<std::shared_ptr<texture_record>> pending_unloads{};
|
||||
std::unordered_map<font_key, Font, font_key_hash> fonts{};
|
||||
};
|
||||
|
||||
class raylib_texture final : public luma::texture {
|
||||
public:
|
||||
raylib_texture(std::shared_ptr<raylib_resource_state::texture_record> record, std::shared_ptr<raylib_resource_state> state);
|
||||
~raylib_texture() override;
|
||||
auto info() const -> luma::texture_info override;
|
||||
auto valid() const -> std::bool_t;
|
||||
auto native() const -> Texture2D;
|
||||
private:
|
||||
std::shared_ptr<raylib_resource_state::texture_record> record_{};
|
||||
std::shared_ptr<raylib_resource_state> state_{};
|
||||
};
|
||||
|
||||
class raylib_resources final : public luma::renderer_resources {
|
||||
public:
|
||||
explicit raylib_resources(std::shared_ptr<raylib_resource_state> state);
|
||||
auto load_texture(std::string const &path) -> std::expected<std::shared_ptr<luma::texture>, modulus::error> override;
|
||||
auto state() const -> std::shared_ptr<raylib_resource_state>;
|
||||
private:
|
||||
std::shared_ptr<raylib_resource_state> state_{};
|
||||
};
|
||||
|
||||
class raylib_draw_context final : public luma::draw_context {
|
||||
public:
|
||||
explicit raylib_draw_context(std::shared_ptr<raylib_resource_state> state);
|
||||
auto fill_rect(luma::rect bounds, luma::color color) -> void override;
|
||||
auto stroke_rect(luma::rect bounds, std::float32_t thickness, luma::color color) -> void override;
|
||||
auto stroke_line(luma::point from, luma::point to, std::float32_t thickness, luma::color color) -> void override;
|
||||
auto fill_ellipse(luma::rect bounds, luma::color color) -> void override;
|
||||
auto stroke_ellipse(luma::rect bounds, std::float32_t thickness, luma::color color) -> void override;
|
||||
auto draw_text(std::string_view text, luma::point origin, luma::text_style const &style, luma::color color) -> void override;
|
||||
auto draw_texture(luma::texture const &texture, luma::rect source, luma::rect destination, luma::color tint) -> void override;
|
||||
auto push_clip(luma::rect bounds) -> void override;
|
||||
auto pop_clip() -> void override;
|
||||
auto measure_text(std::string_view text, luma::text_style const &style) const -> luma::size override;
|
||||
private:
|
||||
std::shared_ptr<raylib_resource_state> state_{};
|
||||
std::vector<luma::rect> clip_stack_{};
|
||||
};
|
||||
|
||||
class scope_exit {
|
||||
public:
|
||||
explicit scope_exit(std::function<void()> action);
|
||||
@@ -146,17 +85,6 @@ private:
|
||||
std::bool_t active_{true};
|
||||
};
|
||||
|
||||
class drawing_scope {
|
||||
public:
|
||||
drawing_scope();
|
||||
drawing_scope(drawing_scope const &) = delete;
|
||||
auto operator=(drawing_scope const &) -> drawing_scope & = delete;
|
||||
~drawing_scope();
|
||||
auto end() -> void;
|
||||
private:
|
||||
std::bool_t active_{true};
|
||||
};
|
||||
|
||||
class ui_traversal_scope {
|
||||
public:
|
||||
explicit ui_traversal_scope(luma::detail::application_core &core);
|
||||
@@ -169,15 +97,6 @@ private:
|
||||
|
||||
constexpr auto max_layout_passes = std::uint32_t{8};
|
||||
|
||||
auto to_raylib(luma::color color) -> Color {
|
||||
return {
|
||||
.r = color.r,
|
||||
.g = color.g,
|
||||
.b = color.b,
|
||||
.a = color.a,
|
||||
};
|
||||
}
|
||||
|
||||
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));
|
||||
@@ -393,151 +312,6 @@ auto cleanup_ui_state(luma::detail::application_core &core) -> void {
|
||||
mark_layout_dirty(core);
|
||||
}
|
||||
|
||||
auto unload_record(raylib_resource_state::texture_record &record) -> void {
|
||||
if (!record.unloaded && record.texture.id != 0) {
|
||||
UnloadTexture(record.texture);
|
||||
record.unloaded = true;
|
||||
record.texture.id = 0;
|
||||
}
|
||||
}
|
||||
|
||||
auto flush_pending_unloads(raylib_resource_state &state) -> std::bool_t {
|
||||
assert(state.ui_thread == std::this_thread::get_id());
|
||||
auto pending = std::vector<std::shared_ptr<raylib_resource_state::texture_record>>{};
|
||||
{
|
||||
auto lock = std::scoped_lock{state.mutex};
|
||||
pending.swap(state.pending_unloads);
|
||||
}
|
||||
auto unloaded = false;
|
||||
for (auto &record : pending) {
|
||||
if (record) {
|
||||
unload_record(*record);
|
||||
unloaded = true;
|
||||
}
|
||||
}
|
||||
return unloaded;
|
||||
}
|
||||
|
||||
auto unload_all_textures(raylib_resource_state &state) -> void {
|
||||
assert(state.ui_thread == std::this_thread::get_id());
|
||||
static_cast<void>(flush_pending_unloads(state));
|
||||
auto records = std::vector<std::shared_ptr<raylib_resource_state::texture_record>>{};
|
||||
{
|
||||
auto lock = std::scoped_lock{state.mutex};
|
||||
auto write = state.records.begin();
|
||||
for (auto read = state.records.begin(); read != state.records.end(); ++read) {
|
||||
if (auto record = read->lock()) {
|
||||
records.push_back(record);
|
||||
*write = *read;
|
||||
++write;
|
||||
}
|
||||
}
|
||||
state.records.erase(write, state.records.end());
|
||||
}
|
||||
for (auto &record : records) {
|
||||
unload_record(*record);
|
||||
}
|
||||
}
|
||||
|
||||
auto unload_all_fonts(raylib_resource_state &state) -> void {
|
||||
assert(state.ui_thread == std::this_thread::get_id());
|
||||
for (auto &[path, font] : state.fonts) {
|
||||
static_cast<void>(path);
|
||||
if (font.texture.id != 0) {
|
||||
UnloadFont(font);
|
||||
}
|
||||
}
|
||||
state.fonts.clear();
|
||||
}
|
||||
|
||||
auto resolve_font(raylib_resource_state &state, luma::text_style const &style) -> Font {
|
||||
assert(state.ui_thread == std::this_thread::get_id());
|
||||
assert(IsWindowReady());
|
||||
if (style.font.empty()) {
|
||||
throw std::runtime_error{"text style font must not be empty"};
|
||||
}
|
||||
if (style.font == "default") {
|
||||
return GetFontDefault();
|
||||
}
|
||||
auto key = font_key{
|
||||
.path = style.font,
|
||||
.size = std::max(std::int32_t{1}, static_cast<std::int32_t>(std::ceil(style.size))),
|
||||
};
|
||||
if (auto found = state.fonts.find(key); found != state.fonts.end()) {
|
||||
return found->second;
|
||||
}
|
||||
auto loaded = LoadFontEx(style.font.c_str(), key.size, nullptr, 0);
|
||||
if (loaded.texture.id == 0) {
|
||||
throw std::runtime_error{"failed to load font: " + style.font};
|
||||
}
|
||||
auto inserted = state.fonts.emplace(std::move(key), loaded);
|
||||
return inserted.first->second;
|
||||
}
|
||||
|
||||
auto intersect_clip(luma::rect left, luma::rect right) -> luma::rect {
|
||||
auto x = std::max(left.x, right.x);
|
||||
auto y = std::max(left.y, right.y);
|
||||
auto end_x = std::min(luma::right(left), luma::right(right));
|
||||
auto end_y = std::min(luma::bottom(left), luma::bottom(right));
|
||||
return {
|
||||
.x = x,
|
||||
.y = y,
|
||||
.width = std::max(std::float32_t{}, end_x - x),
|
||||
.height = std::max(std::float32_t{}, end_y - y),
|
||||
};
|
||||
}
|
||||
|
||||
auto begin_clip(luma::rect bounds) -> void {
|
||||
BeginScissorMode(static_cast<std::int32_t>(bounds.x), static_cast<std::int32_t>(bounds.y), static_cast<std::int32_t>(bounds.width), static_cast<std::int32_t>(bounds.height));
|
||||
}
|
||||
|
||||
auto poll_touch(std::unordered_map<std::int32_t, luma::point> &previous, std::int32_t max_points) -> luma::touch_event {
|
||||
assert(IsWindowReady());
|
||||
assert(max_points > 0);
|
||||
auto current = std::unordered_map<std::int32_t, luma::point>{};
|
||||
auto points = std::vector<luma::touch_point>{};
|
||||
auto count = std::min(GetTouchPointCount(), max_points);
|
||||
for (auto index = 0; index < count; ++index) {
|
||||
auto id = GetTouchPointId(index);
|
||||
auto position = GetTouchPosition(index);
|
||||
auto point = luma::point{
|
||||
.x = position.x,
|
||||
.y = position.y,
|
||||
};
|
||||
auto phase = previous.contains(id) ? luma::touch_phase::MOVE : luma::touch_phase::DOWN;
|
||||
current.emplace(id, point);
|
||||
points.push_back({
|
||||
.id = id,
|
||||
.position = point,
|
||||
.phase = phase,
|
||||
});
|
||||
}
|
||||
for (auto const &[id, position] : previous) {
|
||||
if (!current.contains(id)) {
|
||||
points.push_back({
|
||||
.id = id,
|
||||
.position = position,
|
||||
.phase = luma::touch_phase::UP,
|
||||
});
|
||||
}
|
||||
}
|
||||
previous = std::move(current);
|
||||
return {
|
||||
.points = std::move(points),
|
||||
.time = std::chrono::steady_clock::now(),
|
||||
};
|
||||
}
|
||||
|
||||
auto font_key::operator==(font_key const &other) const -> std::bool_t {
|
||||
return path == other.path && size == other.size;
|
||||
}
|
||||
|
||||
auto font_key_hash::operator()(font_key const &key) const noexcept -> std::size_t {
|
||||
auto seed = std::hash<std::string>{}(key.path);
|
||||
seed ^= std::hash<std::int32_t>{}(key.size) + 0x9e3779b9U + (seed << 6U) + (seed >> 2U);
|
||||
return seed;
|
||||
}
|
||||
|
||||
scope_exit::scope_exit(std::function<void()> action)
|
||||
: action_{std::move(action)} {}
|
||||
|
||||
@@ -551,27 +325,6 @@ scope_exit::~scope_exit() {
|
||||
}
|
||||
}
|
||||
|
||||
drawing_scope::drawing_scope() {
|
||||
BeginDrawing();
|
||||
}
|
||||
|
||||
drawing_scope::~drawing_scope() {
|
||||
if (active_) {
|
||||
end();
|
||||
}
|
||||
}
|
||||
|
||||
auto drawing_scope::end() -> void {
|
||||
if (active_) {
|
||||
EndDrawing();
|
||||
#if defined(LUMA_RAYLIB_CUSTOM_FRAME_CONTROL) && LUMA_RAYLIB_CUSTOM_FRAME_CONTROL
|
||||
SwapScreenBuffer();
|
||||
PollInputEvents();
|
||||
#endif
|
||||
active_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
ui_traversal_scope::ui_traversal_scope(luma::detail::application_core &core)
|
||||
: core_{core} {
|
||||
++core_.traversal_depth;
|
||||
@@ -583,215 +336,6 @@ ui_traversal_scope::~ui_traversal_scope() {
|
||||
flush_retired_controls(core_);
|
||||
}
|
||||
|
||||
raylib_texture::raylib_texture(std::shared_ptr<raylib_resource_state::texture_record> record, std::shared_ptr<raylib_resource_state> state)
|
||||
: record_{std::move(record)}, state_{std::move(state)} {
|
||||
assert(record_ != nullptr);
|
||||
assert(record_->texture.id != 0);
|
||||
assert(record_->texture.width > 0);
|
||||
assert(record_->texture.height > 0);
|
||||
assert(state_ != nullptr);
|
||||
}
|
||||
|
||||
raylib_texture::~raylib_texture() {
|
||||
if (!record_ || record_->unloaded || !state_ || !state_->active.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
if (state_->ui_thread == std::this_thread::get_id()) {
|
||||
unload_record(*record_);
|
||||
return;
|
||||
}
|
||||
auto lock = std::scoped_lock{state_->mutex};
|
||||
state_->pending_unloads.push_back(record_);
|
||||
}
|
||||
|
||||
auto raylib_texture::info() const -> luma::texture_info {
|
||||
assert(record_ != nullptr);
|
||||
return {
|
||||
.width = record_->texture.width,
|
||||
.height = record_->texture.height,
|
||||
};
|
||||
}
|
||||
|
||||
auto raylib_texture::valid() const -> std::bool_t {
|
||||
return record_ != nullptr && !record_->unloaded && record_->texture.id != 0;
|
||||
}
|
||||
|
||||
auto raylib_texture::native() const -> Texture2D {
|
||||
assert(record_ != nullptr);
|
||||
return record_->texture;
|
||||
}
|
||||
|
||||
raylib_resources::raylib_resources(std::shared_ptr<raylib_resource_state> state)
|
||||
: state_{std::move(state)} {
|
||||
assert(state_ != nullptr);
|
||||
}
|
||||
|
||||
auto raylib_resources::load_texture(std::string const &path) -> std::expected<std::shared_ptr<luma::texture>, modulus::error> {
|
||||
assert(state_ != nullptr);
|
||||
assert(state_->active.load(std::memory_order_acquire));
|
||||
assert(state_->ui_thread == std::this_thread::get_id());
|
||||
assert(IsWindowReady());
|
||||
if (path.empty()) {
|
||||
return luma::detail::fail<std::shared_ptr<luma::texture>>(luma::detail::error_invalid_argument, "texture path must not be empty");
|
||||
}
|
||||
auto texture = LoadTexture(path.c_str());
|
||||
if (texture.id == 0) {
|
||||
return luma::detail::fail<std::shared_ptr<luma::texture>>(luma::detail::error_not_found, "failed to load texture: " + path);
|
||||
}
|
||||
auto record = std::make_shared<raylib_resource_state::texture_record>();
|
||||
record->texture = texture;
|
||||
{
|
||||
auto lock = std::scoped_lock{state_->mutex};
|
||||
state_->records.push_back(record);
|
||||
}
|
||||
return modulus::success(std::make_shared<raylib_texture>(record, state_));
|
||||
}
|
||||
|
||||
auto raylib_resources::state() const -> std::shared_ptr<raylib_resource_state> {
|
||||
return state_;
|
||||
}
|
||||
|
||||
raylib_draw_context::raylib_draw_context(std::shared_ptr<raylib_resource_state> state)
|
||||
: state_{std::move(state)} {
|
||||
assert(state_ != nullptr);
|
||||
}
|
||||
|
||||
auto raylib_draw_context::fill_rect(luma::rect bounds, luma::color color) -> void {
|
||||
assert(IsWindowReady());
|
||||
DrawRectangleRec({
|
||||
.x = bounds.x,
|
||||
.y = bounds.y,
|
||||
.width = bounds.width,
|
||||
.height = bounds.height,
|
||||
},
|
||||
to_raylib(color));
|
||||
}
|
||||
|
||||
auto raylib_draw_context::stroke_rect(luma::rect bounds, std::float32_t thickness, luma::color color) -> void {
|
||||
assert(IsWindowReady());
|
||||
assert(thickness >= 0.0F);
|
||||
DrawRectangleLinesEx({
|
||||
.x = bounds.x,
|
||||
.y = bounds.y,
|
||||
.width = bounds.width,
|
||||
.height = bounds.height,
|
||||
},
|
||||
thickness, to_raylib(color));
|
||||
}
|
||||
|
||||
auto raylib_draw_context::stroke_line(luma::point from, luma::point to, std::float32_t thickness, luma::color color) -> void {
|
||||
assert(IsWindowReady());
|
||||
assert(thickness >= 0.0F);
|
||||
DrawLineEx({.x = from.x, .y = from.y}, {.x = to.x, .y = to.y}, thickness, to_raylib(color));
|
||||
}
|
||||
|
||||
auto raylib_draw_context::fill_ellipse(luma::rect bounds, luma::color color) -> void {
|
||||
assert(IsWindowReady());
|
||||
auto center_x = bounds.x + bounds.width * 0.5F;
|
||||
auto center_y = bounds.y + bounds.height * 0.5F;
|
||||
auto radius_x = std::max(std::float32_t{}, bounds.width * 0.5F);
|
||||
auto radius_y = std::max(std::float32_t{}, bounds.height * 0.5F);
|
||||
if (radius_x == 0.0F || radius_y == 0.0F) {
|
||||
return;
|
||||
}
|
||||
DrawEllipse(static_cast<std::int32_t>(std::lround(center_x)), static_cast<std::int32_t>(std::lround(center_y)), radius_x, radius_y, to_raylib(color));
|
||||
}
|
||||
|
||||
auto raylib_draw_context::stroke_ellipse(luma::rect bounds, std::float32_t thickness, luma::color color) -> void {
|
||||
assert(IsWindowReady());
|
||||
assert(thickness >= 0.0F);
|
||||
auto center_x = bounds.x + bounds.width * 0.5F;
|
||||
auto center_y = bounds.y + bounds.height * 0.5F;
|
||||
auto radius_x = std::max(std::float32_t{}, bounds.width * 0.5F);
|
||||
auto radius_y = std::max(std::float32_t{}, bounds.height * 0.5F);
|
||||
if (radius_x == 0.0F || radius_y == 0.0F || thickness == 0.0F) {
|
||||
return;
|
||||
}
|
||||
constexpr auto segments = std::int32_t{72};
|
||||
constexpr auto tau = std::float32_t{6.2831853071795864769F};
|
||||
auto previous = luma::point{.x = center_x + radius_x, .y = center_y};
|
||||
for (auto index = std::int32_t{1}; index <= segments; ++index) {
|
||||
auto angle = tau * static_cast<std::float32_t>(index) / static_cast<std::float32_t>(segments);
|
||||
auto current = luma::point{
|
||||
.x = center_x + std::cos(angle) * radius_x,
|
||||
.y = center_y + std::sin(angle) * radius_y,
|
||||
};
|
||||
DrawLineEx({.x = previous.x, .y = previous.y}, {.x = current.x, .y = current.y}, thickness, to_raylib(color));
|
||||
previous = current;
|
||||
}
|
||||
}
|
||||
|
||||
auto raylib_draw_context::draw_text(std::string_view text, luma::point origin, luma::text_style const &style, luma::color color) -> void {
|
||||
assert(IsWindowReady());
|
||||
assert(state_ != nullptr);
|
||||
assert(style.size > 0.0F);
|
||||
auto owned = std::string{text};
|
||||
auto font = resolve_font(*state_, style);
|
||||
DrawTextEx(font, owned.c_str(), {
|
||||
.x = origin.x,
|
||||
.y = origin.y,
|
||||
},
|
||||
style.size, 1.0F, to_raylib(color));
|
||||
}
|
||||
|
||||
auto raylib_draw_context::draw_texture(luma::texture const &texture, luma::rect source, luma::rect destination, luma::color tint) -> void {
|
||||
assert(IsWindowReady());
|
||||
auto const *native = dynamic_cast<raylib_texture const *>(&texture);
|
||||
if (native == nullptr || !native->valid()) {
|
||||
return;
|
||||
}
|
||||
DrawTexturePro(
|
||||
native->native(),
|
||||
{
|
||||
.x = source.x,
|
||||
.y = source.y,
|
||||
.width = source.width,
|
||||
.height = source.height,
|
||||
},
|
||||
{
|
||||
.x = destination.x,
|
||||
.y = destination.y,
|
||||
.width = destination.width,
|
||||
.height = destination.height,
|
||||
},
|
||||
{
|
||||
.x = 0.0F,
|
||||
.y = 0.0F,
|
||||
},
|
||||
0.0F,
|
||||
to_raylib(tint));
|
||||
}
|
||||
|
||||
auto raylib_draw_context::push_clip(luma::rect bounds) -> void {
|
||||
assert(IsWindowReady());
|
||||
auto clipped = clip_stack_.empty() ? bounds : intersect_clip(clip_stack_.back(), bounds);
|
||||
clip_stack_.push_back(clipped);
|
||||
begin_clip(clipped);
|
||||
}
|
||||
|
||||
auto raylib_draw_context::pop_clip() -> void {
|
||||
assert(IsWindowReady());
|
||||
assert(!clip_stack_.empty());
|
||||
EndScissorMode();
|
||||
clip_stack_.pop_back();
|
||||
if (!clip_stack_.empty()) {
|
||||
begin_clip(clip_stack_.back());
|
||||
}
|
||||
}
|
||||
|
||||
auto raylib_draw_context::measure_text(std::string_view text, luma::text_style const &style) const -> luma::size {
|
||||
assert(IsWindowReady());
|
||||
assert(state_ != nullptr);
|
||||
assert(style.size > 0.0F);
|
||||
auto owned = std::string{text};
|
||||
auto font = resolve_font(*state_, style);
|
||||
auto measured = MeasureTextEx(font, owned.c_str(), style.size, 1.0F);
|
||||
return {
|
||||
.width = measured.x,
|
||||
.height = style.line_height,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
luma::detail::application_core::application_core(luma::application_config value)
|
||||
@@ -1112,7 +656,7 @@ auto luma::application::run() -> std::expected<void, modulus::error> {
|
||||
core->running.store(true, std::memory_order_release);
|
||||
core->accepting_tasks.store(false, std::memory_order_release);
|
||||
}
|
||||
auto resource_state = std::shared_ptr<raylib_resource_state>{};
|
||||
auto resource_state = std::shared_ptr<luma::detail::raylib_resource_state>{};
|
||||
auto window_ready = false;
|
||||
[[maybe_unused]] auto cleanup = scope_exit{[&core, &resource_state, &window_ready] {
|
||||
{
|
||||
@@ -1123,8 +667,8 @@ auto luma::application::run() -> std::expected<void, modulus::error> {
|
||||
drain_ui_tasks(*core);
|
||||
cleanup_ui_state(*core);
|
||||
if (resource_state) {
|
||||
unload_all_textures(*resource_state);
|
||||
unload_all_fonts(*resource_state);
|
||||
luma::detail::unload_all_textures(*resource_state);
|
||||
luma::detail::unload_all_fonts(*resource_state);
|
||||
resource_state->active.store(false, std::memory_order_release);
|
||||
}
|
||||
core->renderer_active.store(false, std::memory_order_release);
|
||||
@@ -1145,24 +689,24 @@ auto luma::application::run() -> std::expected<void, modulus::error> {
|
||||
auto current_target_fps = core->config.display.target_fps;
|
||||
auto const idle_fps = core->config.display.idle_fps;
|
||||
SetTargetFPS(current_target_fps);
|
||||
resource_state = std::make_shared<raylib_resource_state>();
|
||||
resource_state = std::make_shared<luma::detail::raylib_resource_state>();
|
||||
resource_state->ui_thread = std::this_thread::get_id();
|
||||
resource_state->active.store(true, std::memory_order_release);
|
||||
core->renderer_active.store(true, std::memory_order_release);
|
||||
core->resources = std::make_unique<raylib_resources>(resource_state);
|
||||
core->resources = std::make_unique<luma::detail::raylib_resources>(resource_state);
|
||||
core->assets = std::make_unique<luma::asset_cache>(*core->resources);
|
||||
{
|
||||
auto lock = std::scoped_lock{core->lifecycle_mutex};
|
||||
core->accepting_tasks.store(true, std::memory_order_release);
|
||||
}
|
||||
startup_lock.unlock();
|
||||
auto context = raylib_draw_context{resource_state};
|
||||
auto context = luma::detail::raylib_draw_context{resource_state};
|
||||
auto previous_touches = std::unordered_map<std::int32_t, luma::point>{};
|
||||
auto previous_frame = std::chrono::steady_clock::now();
|
||||
try {
|
||||
while (!WindowShouldClose() && !core->stop_requested.load(std::memory_order_acquire)) {
|
||||
auto const ran_ui_tasks = drain_ui_tasks(*core) > 0;
|
||||
if (flush_pending_unloads(*resource_state)) {
|
||||
if (luma::detail::flush_pending_unloads(*resource_state)) {
|
||||
mark_redraw_dirty(*core);
|
||||
}
|
||||
if (ran_ui_tasks) {
|
||||
@@ -1182,7 +726,7 @@ auto luma::application::run() -> std::expected<void, modulus::error> {
|
||||
if (core->overlays != nullptr && core->overlays->tick(delta)) {
|
||||
mark_redraw_dirty(*core);
|
||||
}
|
||||
auto event = poll_touch(previous_touches, core->config.touch.max_points);
|
||||
auto event = luma::detail::poll_touch(previous_touches, core->config.touch.max_points);
|
||||
auto const touch_active = !event.points.empty();
|
||||
if (touch_active) {
|
||||
mark_redraw_dirty(*core);
|
||||
@@ -1250,8 +794,8 @@ auto luma::application::run() -> std::expected<void, modulus::error> {
|
||||
}
|
||||
auto const should_draw = core->redraw_dirty.exchange(false, std::memory_order_acq_rel) || did_layout || animations_active || touch_active;
|
||||
if (should_draw) {
|
||||
auto drawing = drawing_scope{};
|
||||
ClearBackground(to_raylib(core->config.theme.background));
|
||||
auto drawing = luma::detail::drawing_scope{};
|
||||
ClearBackground(luma::detail::to_raylib(core->config.theme.background));
|
||||
auto *active = active_control(*core);
|
||||
if (active != nullptr) {
|
||||
active->draw(context, core->config.theme);
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
#include "detail/control_types.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
|
||||
namespace luma::detail {
|
||||
|
||||
label_control::label_control(std::string text)
|
||||
: text_{std::move(text)} {
|
||||
assert(!text_.empty());
|
||||
}
|
||||
|
||||
auto label_control::measure(luma::draw_context &context, luma::theme const &theme, luma::constraints limits) -> luma::size {
|
||||
assert(finite_constraints(limits));
|
||||
auto measured = context.measure_text(text_, theme.body);
|
||||
assert(finite_size(measured));
|
||||
return luma::clamp(measured, limits);
|
||||
}
|
||||
|
||||
auto label_control::draw(luma::draw_context &context, luma::theme const &theme) const -> void {
|
||||
assert(!text_.empty());
|
||||
context.draw_text(text_, {
|
||||
.x = bounds().x,
|
||||
.y = bounds().y,
|
||||
},
|
||||
theme.body, enabled() ? theme.text : theme.text_muted);
|
||||
}
|
||||
|
||||
button_control::button_control(std::string text, std::function<void()> on_click)
|
||||
: text_{std::move(text)}, on_click_{std::move(on_click)} {
|
||||
assert(!text_.empty());
|
||||
assert(on_click_);
|
||||
}
|
||||
|
||||
auto button_control::measure(luma::draw_context &context, luma::theme const &theme, luma::constraints limits) -> luma::size {
|
||||
assert(finite_constraints(limits));
|
||||
auto text_size = context.measure_text(text_, theme.body);
|
||||
assert(finite_size(text_size));
|
||||
return luma::clamp({
|
||||
.width = text_size.width + 32.0F,
|
||||
.height = std::max(std::float32_t{48}, text_size.height + std::float32_t{16}),
|
||||
},
|
||||
limits);
|
||||
}
|
||||
|
||||
auto button_control::draw(luma::draw_context &context, luma::theme const &theme) const -> void {
|
||||
auto fill = pressed_id_.has_value() ? theme.surface_active : theme.surface;
|
||||
context.fill_rect(bounds(), fill);
|
||||
context.stroke_rect(bounds(), 1.0F, focused() ? theme.accent : theme.border);
|
||||
context.draw_text(text_, centered_text_origin(context, text_, bounds(), theme.body), theme.body, enabled() ? theme.text : theme.text_muted);
|
||||
}
|
||||
|
||||
auto button_control::handle_touch(luma::touch_event const &event) -> std::bool_t {
|
||||
if (!enabled()) {
|
||||
return false;
|
||||
}
|
||||
auto const *point = pressed_id_ ? find_point(event, *pressed_id_) : first_point_inside(event, bounds(), luma::touch_phase::DOWN);
|
||||
if (point == nullptr) {
|
||||
return false;
|
||||
}
|
||||
assert(std::isfinite(point->position.x));
|
||||
assert(std::isfinite(point->position.y));
|
||||
if (point->phase == luma::touch_phase::DOWN && luma::contains(bounds(), point->position)) {
|
||||
pressed_id_ = point->id;
|
||||
return true;
|
||||
}
|
||||
if (pressed_id_ == point->id && point->phase == luma::touch_phase::UP) {
|
||||
auto inside = luma::contains(bounds(), point->position);
|
||||
pressed_id_.reset();
|
||||
if (inside) {
|
||||
assert(on_click_);
|
||||
on_click_();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (pressed_id_ == point->id && point->phase == luma::touch_phase::CANCEL) {
|
||||
pressed_id_.reset();
|
||||
return true;
|
||||
}
|
||||
return pressed_id_ == point->id;
|
||||
}
|
||||
|
||||
auto button_control::focusable() const -> std::bool_t {
|
||||
return true;
|
||||
}
|
||||
|
||||
spacer_control::spacer_control(luma::size value)
|
||||
: value_{value} {
|
||||
assert(finite_size(value_));
|
||||
}
|
||||
|
||||
auto spacer_control::measure(luma::draw_context &, luma::theme const &, luma::constraints limits) -> luma::size {
|
||||
assert(finite_constraints(limits));
|
||||
return luma::clamp(value_, limits);
|
||||
}
|
||||
|
||||
auto spacer_control::draw(luma::draw_context &, luma::theme const &) const -> void {}
|
||||
|
||||
progress_bar_control::progress_bar_control(std::float32_t value)
|
||||
: value_{std::clamp(value, std::float32_t{}, std::float32_t{1})} {
|
||||
assert(std::isfinite(value));
|
||||
}
|
||||
|
||||
auto progress_bar_control::measure(luma::draw_context &, luma::theme const &, luma::constraints limits) -> luma::size {
|
||||
assert(finite_constraints(limits));
|
||||
return luma::clamp({
|
||||
.width = 160.0F,
|
||||
.height = 16.0F,
|
||||
},
|
||||
limits);
|
||||
}
|
||||
|
||||
auto progress_bar_control::draw(luma::draw_context &context, luma::theme const &theme) const -> void {
|
||||
context.fill_rect(bounds(), theme.surface);
|
||||
context.fill_rect({
|
||||
.x = bounds().x,
|
||||
.y = bounds().y,
|
||||
.width = bounds().width * value_,
|
||||
.height = bounds().height,
|
||||
},
|
||||
theme.accent);
|
||||
}
|
||||
|
||||
image_control::image_control(std::shared_ptr<luma::texture> texture, luma::image_fit fit)
|
||||
: texture_{std::move(texture)}, fit_{fit} {
|
||||
assert(texture_ != nullptr);
|
||||
}
|
||||
|
||||
auto image_control::measure(luma::draw_context &, luma::theme const &, luma::constraints limits) -> luma::size {
|
||||
assert(finite_constraints(limits));
|
||||
auto info = texture_->info();
|
||||
if (info.width <= 0 || info.height <= 0) {
|
||||
return luma::clamp({}, limits);
|
||||
}
|
||||
return luma::clamp({
|
||||
.width = static_cast<std::float32_t>(info.width),
|
||||
.height = static_cast<std::float32_t>(info.height),
|
||||
},
|
||||
limits);
|
||||
}
|
||||
|
||||
auto image_control::draw(luma::draw_context &context, luma::theme const &theme) const -> void {
|
||||
auto info = texture_->info();
|
||||
if (info.width <= 0 || info.height <= 0) {
|
||||
return;
|
||||
}
|
||||
auto source = luma::rect{
|
||||
.x = 0.0F,
|
||||
.y = 0.0F,
|
||||
.width = static_cast<std::float32_t>(info.width),
|
||||
.height = static_cast<std::float32_t>(info.height),
|
||||
};
|
||||
context.draw_texture(*texture_, source, fit_rect(bounds(), info, fit_), theme.text);
|
||||
}
|
||||
|
||||
}
|
||||
+36
-1511
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,168 @@
|
||||
#include "control_types.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <stdexcept>
|
||||
|
||||
|
||||
namespace luma::detail {
|
||||
|
||||
auto finite_size(luma::size value) -> std::bool_t {
|
||||
return std::isfinite(value.width) && std::isfinite(value.height) && value.width >= 0.0F && value.height >= 0.0F;
|
||||
}
|
||||
|
||||
auto finite_rect(luma::rect value) -> std::bool_t {
|
||||
return std::isfinite(value.x) && std::isfinite(value.y) && finite_size({
|
||||
.width = value.width,
|
||||
.height = value.height,
|
||||
});
|
||||
}
|
||||
|
||||
auto finite_constraints(luma::constraints value) -> std::bool_t {
|
||||
return finite_size(value.min) && finite_size(value.max) && value.min.width <= value.max.width && value.min.height <= value.max.height;
|
||||
}
|
||||
|
||||
auto same_size(luma::size left, luma::size right) -> std::bool_t {
|
||||
return left.width == right.width && left.height == right.height;
|
||||
}
|
||||
|
||||
auto same_constraints(luma::constraints left, luma::constraints right) -> std::bool_t {
|
||||
return same_size(left.min, right.min) && same_size(left.max, right.max);
|
||||
}
|
||||
|
||||
auto same_text_style(luma::text_style const &left, luma::text_style const &right) -> std::bool_t {
|
||||
return left.font == right.font && left.size == right.size && left.line_height == right.line_height;
|
||||
}
|
||||
|
||||
auto same_theme_metrics(luma::theme const &left, luma::theme const &right) -> std::bool_t {
|
||||
return same_text_style(left.body, right.body) && same_text_style(left.title, right.title) && left.radius == right.radius && left.spacing == right.spacing;
|
||||
}
|
||||
|
||||
auto throw_error(modulus::error const &error) -> void {
|
||||
throw std::runtime_error(error.describe());
|
||||
}
|
||||
|
||||
auto throw_if_failed(std::expected<void, modulus::error> const &status) -> void {
|
||||
if (!status) {
|
||||
throw_error(status.error());
|
||||
}
|
||||
}
|
||||
|
||||
auto make_event(luma::touch_event const &event, luma::touch_point point) -> luma::touch_event {
|
||||
return {
|
||||
.points = {point},
|
||||
.time = event.time,
|
||||
};
|
||||
}
|
||||
|
||||
auto dispatch_to_children(std::vector<std::unique_ptr<luma::control>> &children, luma::touch_event const &event) -> std::bool_t {
|
||||
auto handled = false;
|
||||
for (auto const &point : event.points) {
|
||||
auto single = make_event(event, point);
|
||||
for (auto current = children.rbegin(); current != children.rend(); ++current) {
|
||||
assert(*current != nullptr);
|
||||
if ((*current)->handle_touch(single)) {
|
||||
handled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return handled;
|
||||
}
|
||||
|
||||
auto find_point(luma::touch_event const &event, std::int32_t id) -> luma::touch_point const * {
|
||||
for (auto const &point : event.points) {
|
||||
if (point.id == id) {
|
||||
return &point;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto first_point_with_phase(luma::touch_event const &event, luma::touch_phase phase) -> luma::touch_point const * {
|
||||
for (auto const &point : event.points) {
|
||||
if (point.phase == phase) {
|
||||
return &point;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
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 valid_keyboard_layout(luma::keyboard_layout const &layout) -> std::expected<void, modulus::error> {
|
||||
if (layout.rows.empty()) {
|
||||
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "keyboard layout must contain at least one row");
|
||||
}
|
||||
auto has_key = false;
|
||||
for (auto const &row : layout.rows) {
|
||||
for (auto const &key : row) {
|
||||
has_key = true;
|
||||
if (key.label.empty()) {
|
||||
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "keyboard key label must not be empty");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!has_key) {
|
||||
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "keyboard layout must contain at least one key");
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto validate_children(std::vector<std::unique_ptr<luma::control>> const &children) -> std::expected<void, modulus::error> {
|
||||
for (auto const &child : children) {
|
||||
if (child == nullptr) {
|
||||
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "child control must not be null");
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto centered_text_origin(luma::draw_context &context, std::string const &text, luma::rect bounds, luma::text_style const &style) -> luma::point {
|
||||
assert(finite_rect(bounds));
|
||||
auto measured = context.measure_text(text, style);
|
||||
assert(finite_size(measured));
|
||||
return {
|
||||
.x = bounds.x + (bounds.width - measured.width) * 0.5F,
|
||||
.y = bounds.y + (bounds.height - measured.height) * 0.5F,
|
||||
};
|
||||
}
|
||||
|
||||
auto fit_rect(luma::rect bounds, luma::texture_info info, luma::image_fit fit) -> luma::rect {
|
||||
assert(finite_rect(bounds));
|
||||
assert(info.width > 0);
|
||||
assert(info.height > 0);
|
||||
auto source_width = static_cast<std::float32_t>(info.width);
|
||||
auto source_height = static_cast<std::float32_t>(info.height);
|
||||
if (fit == luma::image_fit::FILL) {
|
||||
return bounds;
|
||||
}
|
||||
if (fit == luma::image_fit::CENTER) {
|
||||
return {
|
||||
.x = bounds.x + (bounds.width - source_width) * 0.5F,
|
||||
.y = bounds.y + (bounds.height - source_height) * 0.5F,
|
||||
.width = source_width,
|
||||
.height = source_height,
|
||||
};
|
||||
}
|
||||
auto scale_x = bounds.width / source_width;
|
||||
auto scale_y = bounds.height / source_height;
|
||||
auto scale = fit == luma::image_fit::COVER ? std::max(scale_x, scale_y) : std::min(scale_x, scale_y);
|
||||
auto width = source_width * scale;
|
||||
auto height = source_height * scale;
|
||||
return {
|
||||
.x = bounds.x + (bounds.width - width) * 0.5F,
|
||||
.y = bounds.y + (bounds.height - height) * 0.5F,
|
||||
.width = width,
|
||||
.height = height,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <modulus.hpp>
|
||||
#include <luma/controls.hpp>
|
||||
#include <luma/detail/errors.hpp>
|
||||
#include <luma/geometry.hpp>
|
||||
#include <luma/input.hpp>
|
||||
#include <luma/render.hpp>
|
||||
#include <luma/theme.hpp>
|
||||
|
||||
|
||||
namespace luma::detail {
|
||||
|
||||
constexpr auto panel_padding = luma::edge_insets{
|
||||
.left = 8.0F,
|
||||
.top = 8.0F,
|
||||
.right = 8.0F,
|
||||
.bottom = 8.0F};
|
||||
constexpr auto scroll_drag_threshold = std::float32_t{6.0F};
|
||||
|
||||
class label_control final : public luma::control {
|
||||
public:
|
||||
explicit label_control(std::string text);
|
||||
auto measure(luma::draw_context &context, luma::theme const &theme, luma::constraints limits) -> luma::size override;
|
||||
auto draw(luma::draw_context &context, luma::theme const &theme) const -> void override;
|
||||
private:
|
||||
std::string text_;
|
||||
};
|
||||
|
||||
class button_control final : public luma::control {
|
||||
public:
|
||||
button_control(std::string text, std::function<void()> on_click);
|
||||
auto measure(luma::draw_context &context, luma::theme const &theme, luma::constraints limits) -> luma::size override;
|
||||
auto draw(luma::draw_context &context, luma::theme const &theme) const -> void override;
|
||||
auto handle_touch(luma::touch_event const &event) -> std::bool_t override;
|
||||
auto focusable() const -> std::bool_t override;
|
||||
private:
|
||||
std::string text_;
|
||||
std::function<void()> on_click_;
|
||||
std::optional<std::int32_t> pressed_id_;
|
||||
};
|
||||
|
||||
class text_field_control final : public luma::control {
|
||||
public:
|
||||
explicit text_field_control(std::shared_ptr<luma::text_input_state> input);
|
||||
auto measure(luma::draw_context &context, luma::theme const &theme, luma::constraints limits) -> luma::size override;
|
||||
auto draw(luma::draw_context &context, luma::theme const &theme) const -> void override;
|
||||
auto handle_touch(luma::touch_event const &event) -> std::bool_t override;
|
||||
auto invalidate_layout() -> void override;
|
||||
auto bind_layout_invalidator(std::function<void()> invalidator) -> void override;
|
||||
auto focusable() const -> std::bool_t override;
|
||||
private:
|
||||
std::shared_ptr<luma::text_input_state> input_;
|
||||
std::shared_ptr<void> layout_listener_lifetime_{};
|
||||
mutable std::optional<luma::constraints> cached_limits_{};
|
||||
mutable std::optional<luma::text_style> cached_style_{};
|
||||
mutable std::uint64_t cached_version_{};
|
||||
mutable luma::size cached_size_{};
|
||||
};
|
||||
|
||||
class soft_keyboard_control final : public luma::control {
|
||||
public:
|
||||
soft_keyboard_control(luma::keyboard_layout layout, std::function<void(std::string)> on_text, std::function<void()> on_enter);
|
||||
soft_keyboard_control(luma::keyboard_layout layout, std::shared_ptr<luma::text_input_state> input, std::function<void()> on_enter);
|
||||
auto measure(luma::draw_context &, luma::theme const &, luma::constraints limits) -> luma::size override;
|
||||
auto draw(luma::draw_context &context, luma::theme const &theme) const -> void override;
|
||||
auto handle_touch(luma::touch_event const &event) -> std::bool_t override;
|
||||
private:
|
||||
luma::keyboard_layout layout_;
|
||||
std::function<void(std::string)> on_text_;
|
||||
std::function<void()> on_enter_;
|
||||
std::shared_ptr<luma::text_input_state> input_{};
|
||||
};
|
||||
|
||||
class panel_control final : public luma::control {
|
||||
public:
|
||||
explicit panel_control(std::vector<std::unique_ptr<luma::control>> children);
|
||||
auto measure(luma::draw_context &context, luma::theme const &theme, luma::constraints limits) -> luma::size override;
|
||||
auto arrange(luma::rect bounds) -> void override;
|
||||
auto draw(luma::draw_context &context, luma::theme const &theme) const -> void override;
|
||||
auto handle_touch(luma::touch_event const &event) -> std::bool_t override;
|
||||
auto invalidate_layout() -> void override;
|
||||
auto bind_layout_invalidator(std::function<void()> invalidator) -> void override;
|
||||
private:
|
||||
std::vector<std::unique_ptr<luma::control>> children_;
|
||||
};
|
||||
|
||||
class spacer_control final : public luma::control {
|
||||
public:
|
||||
explicit spacer_control(luma::size value);
|
||||
auto measure(luma::draw_context &, luma::theme const &, luma::constraints limits) -> luma::size override;
|
||||
auto draw(luma::draw_context &, luma::theme const &) const -> void override;
|
||||
private:
|
||||
luma::size value_;
|
||||
};
|
||||
|
||||
class progress_bar_control final : public luma::control {
|
||||
public:
|
||||
explicit progress_bar_control(std::float32_t value);
|
||||
auto measure(luma::draw_context &, luma::theme const &, luma::constraints limits) -> luma::size override;
|
||||
auto draw(luma::draw_context &context, luma::theme const &theme) const -> void override;
|
||||
private:
|
||||
std::float32_t value_;
|
||||
};
|
||||
|
||||
class image_control final : public luma::control {
|
||||
public:
|
||||
image_control(std::shared_ptr<luma::texture> texture, luma::image_fit fit);
|
||||
auto measure(luma::draw_context &, luma::theme const &, luma::constraints limits) -> luma::size override;
|
||||
auto draw(luma::draw_context &context, luma::theme const &theme) const -> void override;
|
||||
private:
|
||||
std::shared_ptr<luma::texture> texture_;
|
||||
luma::image_fit fit_;
|
||||
};
|
||||
|
||||
class linear_control final : public luma::control {
|
||||
public:
|
||||
linear_control(luma::direction direction, std::vector<std::unique_ptr<luma::control>> children, std::float32_t gap, luma::alignment align);
|
||||
auto measure(luma::draw_context &context, luma::theme const &theme, luma::constraints limits) -> luma::size override;
|
||||
auto arrange(luma::rect bounds) -> void override;
|
||||
auto draw(luma::draw_context &context, luma::theme const &theme) const -> void override;
|
||||
auto handle_touch(luma::touch_event const &event) -> std::bool_t override;
|
||||
auto invalidate_layout() -> void override;
|
||||
auto bind_layout_invalidator(std::function<void()> invalidator) -> void override;
|
||||
private:
|
||||
luma::direction direction_;
|
||||
std::vector<std::unique_ptr<luma::control>> children_;
|
||||
std::float32_t gap_;
|
||||
luma::alignment align_;
|
||||
std::vector<luma::size> measured_;
|
||||
};
|
||||
|
||||
class stack_control final : public luma::control {
|
||||
public:
|
||||
explicit stack_control(std::vector<std::unique_ptr<luma::control>> children);
|
||||
auto measure(luma::draw_context &context, luma::theme const &theme, luma::constraints limits) -> luma::size override;
|
||||
auto arrange(luma::rect bounds) -> void override;
|
||||
auto draw(luma::draw_context &context, luma::theme const &theme) const -> void override;
|
||||
auto handle_touch(luma::touch_event const &event) -> std::bool_t override;
|
||||
auto invalidate_layout() -> void override;
|
||||
auto bind_layout_invalidator(std::function<void()> invalidator) -> void override;
|
||||
private:
|
||||
std::vector<std::unique_ptr<luma::control>> children_;
|
||||
};
|
||||
|
||||
class scroll_view_control final : public luma::control {
|
||||
public:
|
||||
scroll_view_control(std::unique_ptr<luma::control> child, std::bool_t vertical, std::bool_t horizontal);
|
||||
auto measure(luma::draw_context &context, luma::theme const &theme, luma::constraints limits) -> luma::size override;
|
||||
auto arrange(luma::rect bounds) -> void override;
|
||||
auto draw(luma::draw_context &context, luma::theme const &theme) const -> void override;
|
||||
auto handle_touch(luma::touch_event const &event) -> std::bool_t override;
|
||||
auto invalidate_layout() -> void override;
|
||||
auto bind_layout_invalidator(std::function<void()> invalidator) -> void override;
|
||||
private:
|
||||
struct touch_state {
|
||||
luma::point start{};
|
||||
luma::point last{};
|
||||
std::bool_t child_active{false};
|
||||
};
|
||||
auto handle_point(luma::touch_event const &event, luma::touch_point const &point) -> std::bool_t;
|
||||
auto invalidate_child_measure() -> void;
|
||||
auto arrange_child() -> void;
|
||||
auto clamp_offset() -> void;
|
||||
std::unique_ptr<luma::control> child_;
|
||||
std::bool_t vertical_;
|
||||
std::bool_t horizontal_;
|
||||
luma::size content_size_{};
|
||||
std::optional<luma::constraints> cached_child_limits_{};
|
||||
std::optional<luma::theme> cached_theme_{};
|
||||
luma::size cached_content_size_{};
|
||||
std::bool_t content_measure_valid_{false};
|
||||
luma::point offset_{};
|
||||
luma::point velocity_{};
|
||||
std::unordered_map<std::int32_t, scroll_view_control::touch_state> touches_{};
|
||||
std::optional<std::int32_t> scroll_id_{};
|
||||
std::bool_t dragging_{false};
|
||||
};
|
||||
|
||||
auto finite_size(luma::size value) -> std::bool_t;
|
||||
auto finite_rect(luma::rect value) -> std::bool_t;
|
||||
auto finite_constraints(luma::constraints value) -> std::bool_t;
|
||||
auto same_size(luma::size left, luma::size right) -> std::bool_t;
|
||||
auto same_constraints(luma::constraints left, luma::constraints right) -> std::bool_t;
|
||||
auto same_text_style(luma::text_style const &left, luma::text_style const &right) -> std::bool_t;
|
||||
auto same_theme_metrics(luma::theme const &left, luma::theme const &right) -> std::bool_t;
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
auto throw_error(modulus::error const &error) -> void;
|
||||
auto throw_if_failed(std::expected<void, modulus::error> const &status) -> void;
|
||||
auto make_event(luma::touch_event const &event, luma::touch_point point) -> luma::touch_event;
|
||||
auto dispatch_to_children(std::vector<std::unique_ptr<luma::control>> &children, luma::touch_event const &event) -> std::bool_t;
|
||||
auto find_point(luma::touch_event const &event, std::int32_t id) -> luma::touch_point const *;
|
||||
auto first_point_with_phase(luma::touch_event const &event, luma::touch_phase phase) -> luma::touch_point const *;
|
||||
auto first_point_inside(luma::touch_event const &event, luma::rect bounds, luma::touch_phase phase) -> luma::touch_point const *;
|
||||
auto valid_keyboard_layout(luma::keyboard_layout const &layout) -> std::expected<void, modulus::error>;
|
||||
auto validate_children(std::vector<std::unique_ptr<luma::control>> const &children) -> std::expected<void, modulus::error>;
|
||||
auto centered_text_origin(luma::draw_context &context, std::string const &text, luma::rect bounds, luma::text_style const &style) -> luma::point;
|
||||
auto fit_rect(luma::rect bounds, luma::texture_info info, luma::image_fit fit) -> luma::rect;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
#include "raylib_backend.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
#include <modulus/log.hpp>
|
||||
#include <luma/detail/errors.hpp>
|
||||
|
||||
|
||||
namespace luma::detail {
|
||||
|
||||
namespace {
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
template <typename VALUE>
|
||||
auto internal(std::string message) -> std::expected<VALUE, modulus::error> {
|
||||
return luma::detail::fail<VALUE>(luma::detail::error_internal, std::move(message));
|
||||
}
|
||||
|
||||
auto internal_error(std::string message) -> modulus::error {
|
||||
return luma::detail::make_error(luma::detail::error_internal, std::move(message));
|
||||
}
|
||||
|
||||
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 to_raylib(luma::color color) -> Color {
|
||||
return {
|
||||
.r = color.r,
|
||||
.g = color.g,
|
||||
.b = color.b,
|
||||
.a = color.a,
|
||||
};
|
||||
}
|
||||
|
||||
auto unload_record(raylib_resource_state::texture_record &record) -> void {
|
||||
if (!record.unloaded && record.texture.id != 0) {
|
||||
UnloadTexture(record.texture);
|
||||
record.unloaded = true;
|
||||
record.texture.id = 0;
|
||||
}
|
||||
}
|
||||
|
||||
auto flush_pending_unloads(raylib_resource_state &state) -> std::bool_t {
|
||||
assert(state.ui_thread == std::this_thread::get_id());
|
||||
auto pending = std::vector<std::shared_ptr<raylib_resource_state::texture_record>>{};
|
||||
{
|
||||
auto lock = std::scoped_lock{state.mutex};
|
||||
pending.swap(state.pending_unloads);
|
||||
}
|
||||
auto unloaded = false;
|
||||
for (auto &record : pending) {
|
||||
if (record) {
|
||||
unload_record(*record);
|
||||
unloaded = true;
|
||||
}
|
||||
}
|
||||
return unloaded;
|
||||
}
|
||||
|
||||
auto unload_all_textures(raylib_resource_state &state) -> void {
|
||||
assert(state.ui_thread == std::this_thread::get_id());
|
||||
static_cast<void>(flush_pending_unloads(state));
|
||||
auto records = std::vector<std::shared_ptr<raylib_resource_state::texture_record>>{};
|
||||
{
|
||||
auto lock = std::scoped_lock{state.mutex};
|
||||
auto write = state.records.begin();
|
||||
for (auto read = state.records.begin(); read != state.records.end(); ++read) {
|
||||
if (auto record = read->lock()) {
|
||||
records.push_back(record);
|
||||
*write = *read;
|
||||
++write;
|
||||
}
|
||||
}
|
||||
state.records.erase(write, state.records.end());
|
||||
}
|
||||
for (auto &record : records) {
|
||||
unload_record(*record);
|
||||
}
|
||||
}
|
||||
|
||||
auto unload_all_fonts(raylib_resource_state &state) -> void {
|
||||
assert(state.ui_thread == std::this_thread::get_id());
|
||||
for (auto &[path, font] : state.fonts) {
|
||||
static_cast<void>(path);
|
||||
if (font.texture.id != 0) {
|
||||
UnloadFont(font);
|
||||
}
|
||||
}
|
||||
state.fonts.clear();
|
||||
}
|
||||
|
||||
auto resolve_font(raylib_resource_state &state, luma::text_style const &style) -> Font {
|
||||
assert(state.ui_thread == std::this_thread::get_id());
|
||||
assert(IsWindowReady());
|
||||
if (style.font.empty()) {
|
||||
throw std::runtime_error{"text style font must not be empty"};
|
||||
}
|
||||
if (style.font == "default") {
|
||||
return GetFontDefault();
|
||||
}
|
||||
auto key = font_key{
|
||||
.path = style.font,
|
||||
.size = std::max(std::int32_t{1}, static_cast<std::int32_t>(std::ceil(style.size))),
|
||||
};
|
||||
if (auto found = state.fonts.find(key); found != state.fonts.end()) {
|
||||
return found->second;
|
||||
}
|
||||
auto loaded = LoadFontEx(style.font.c_str(), key.size, nullptr, 0);
|
||||
if (loaded.texture.id == 0) {
|
||||
throw std::runtime_error{"failed to load font: " + style.font};
|
||||
}
|
||||
auto inserted = state.fonts.emplace(std::move(key), loaded);
|
||||
return inserted.first->second;
|
||||
}
|
||||
|
||||
auto intersect_clip(luma::rect left, luma::rect right) -> luma::rect {
|
||||
auto x = std::max(left.x, right.x);
|
||||
auto y = std::max(left.y, right.y);
|
||||
auto end_x = std::min(luma::right(left), luma::right(right));
|
||||
auto end_y = std::min(luma::bottom(left), luma::bottom(right));
|
||||
return {
|
||||
.x = x,
|
||||
.y = y,
|
||||
.width = std::max(std::float32_t{}, end_x - x),
|
||||
.height = std::max(std::float32_t{}, end_y - y),
|
||||
};
|
||||
}
|
||||
|
||||
auto begin_clip(luma::rect bounds) -> void {
|
||||
BeginScissorMode(static_cast<std::int32_t>(bounds.x), static_cast<std::int32_t>(bounds.y), static_cast<std::int32_t>(bounds.width), static_cast<std::int32_t>(bounds.height));
|
||||
}
|
||||
|
||||
auto poll_touch(std::unordered_map<std::int32_t, luma::point> &previous, std::int32_t max_points) -> luma::touch_event {
|
||||
assert(IsWindowReady());
|
||||
assert(max_points > 0);
|
||||
auto current = std::unordered_map<std::int32_t, luma::point>{};
|
||||
auto points = std::vector<luma::touch_point>{};
|
||||
auto count = std::min(GetTouchPointCount(), max_points);
|
||||
for (auto index = 0; index < count; ++index) {
|
||||
auto id = GetTouchPointId(index);
|
||||
auto position = GetTouchPosition(index);
|
||||
auto point = luma::point{
|
||||
.x = position.x,
|
||||
.y = position.y,
|
||||
};
|
||||
auto phase = previous.contains(id) ? luma::touch_phase::MOVE : luma::touch_phase::DOWN;
|
||||
current.emplace(id, point);
|
||||
points.push_back({
|
||||
.id = id,
|
||||
.position = point,
|
||||
.phase = phase,
|
||||
});
|
||||
}
|
||||
for (auto const &[id, position] : previous) {
|
||||
if (!current.contains(id)) {
|
||||
points.push_back({
|
||||
.id = id,
|
||||
.position = position,
|
||||
.phase = luma::touch_phase::UP,
|
||||
});
|
||||
}
|
||||
}
|
||||
previous = std::move(current);
|
||||
return {
|
||||
.points = std::move(points),
|
||||
.time = std::chrono::steady_clock::now(),
|
||||
};
|
||||
}
|
||||
|
||||
auto font_key::operator==(font_key const &other) const -> std::bool_t {
|
||||
return path == other.path && size == other.size;
|
||||
}
|
||||
|
||||
auto font_key_hash::operator()(font_key const &key) const noexcept -> std::size_t {
|
||||
auto seed = std::hash<std::string>{}(key.path);
|
||||
seed ^= std::hash<std::int32_t>{}(key.size) + 0x9e3779b9U + (seed << 6U) + (seed >> 2U);
|
||||
return seed;
|
||||
}
|
||||
|
||||
drawing_scope::drawing_scope() {
|
||||
BeginDrawing();
|
||||
}
|
||||
|
||||
drawing_scope::~drawing_scope() {
|
||||
if (active_) {
|
||||
end();
|
||||
}
|
||||
}
|
||||
|
||||
auto drawing_scope::end() -> void {
|
||||
if (active_) {
|
||||
EndDrawing();
|
||||
#if defined(LUMA_RAYLIB_CUSTOM_FRAME_CONTROL) && LUMA_RAYLIB_CUSTOM_FRAME_CONTROL
|
||||
SwapScreenBuffer();
|
||||
PollInputEvents();
|
||||
#endif
|
||||
active_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
raylib_texture::raylib_texture(std::shared_ptr<raylib_resource_state::texture_record> record, std::shared_ptr<raylib_resource_state> state)
|
||||
: record_{std::move(record)}, state_{std::move(state)} {
|
||||
assert(record_ != nullptr);
|
||||
assert(record_->texture.id != 0);
|
||||
assert(record_->texture.width > 0);
|
||||
assert(record_->texture.height > 0);
|
||||
assert(state_ != nullptr);
|
||||
}
|
||||
|
||||
raylib_texture::~raylib_texture() {
|
||||
if (!record_ || record_->unloaded || !state_ || !state_->active.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
if (state_->ui_thread == std::this_thread::get_id()) {
|
||||
unload_record(*record_);
|
||||
return;
|
||||
}
|
||||
auto lock = std::scoped_lock{state_->mutex};
|
||||
state_->pending_unloads.push_back(record_);
|
||||
}
|
||||
|
||||
auto raylib_texture::info() const -> luma::texture_info {
|
||||
assert(record_ != nullptr);
|
||||
return {
|
||||
.width = record_->texture.width,
|
||||
.height = record_->texture.height,
|
||||
};
|
||||
}
|
||||
|
||||
auto raylib_texture::valid() const -> std::bool_t {
|
||||
return record_ != nullptr && !record_->unloaded && record_->texture.id != 0;
|
||||
}
|
||||
|
||||
auto raylib_texture::native() const -> Texture2D {
|
||||
assert(record_ != nullptr);
|
||||
return record_->texture;
|
||||
}
|
||||
|
||||
raylib_resources::raylib_resources(std::shared_ptr<raylib_resource_state> state)
|
||||
: state_{std::move(state)} {
|
||||
assert(state_ != nullptr);
|
||||
}
|
||||
|
||||
auto raylib_resources::load_texture(std::string const &path) -> std::expected<std::shared_ptr<luma::texture>, modulus::error> {
|
||||
assert(state_ != nullptr);
|
||||
assert(state_->active.load(std::memory_order_acquire));
|
||||
assert(state_->ui_thread == std::this_thread::get_id());
|
||||
assert(IsWindowReady());
|
||||
if (path.empty()) {
|
||||
return luma::detail::fail<std::shared_ptr<luma::texture>>(luma::detail::error_invalid_argument, "texture path must not be empty");
|
||||
}
|
||||
auto texture = LoadTexture(path.c_str());
|
||||
if (texture.id == 0) {
|
||||
return luma::detail::fail<std::shared_ptr<luma::texture>>(luma::detail::error_not_found, "failed to load texture: " + path);
|
||||
}
|
||||
auto record = std::make_shared<raylib_resource_state::texture_record>();
|
||||
record->texture = texture;
|
||||
{
|
||||
auto lock = std::scoped_lock{state_->mutex};
|
||||
state_->records.push_back(record);
|
||||
}
|
||||
return modulus::success(std::make_shared<raylib_texture>(record, state_));
|
||||
}
|
||||
|
||||
auto raylib_resources::state() const -> std::shared_ptr<raylib_resource_state> {
|
||||
return state_;
|
||||
}
|
||||
|
||||
raylib_draw_context::raylib_draw_context(std::shared_ptr<raylib_resource_state> state)
|
||||
: state_{std::move(state)} {
|
||||
assert(state_ != nullptr);
|
||||
}
|
||||
|
||||
auto raylib_draw_context::fill_rect(luma::rect bounds, luma::color color) -> void {
|
||||
assert(IsWindowReady());
|
||||
DrawRectangleRec({
|
||||
.x = bounds.x,
|
||||
.y = bounds.y,
|
||||
.width = bounds.width,
|
||||
.height = bounds.height,
|
||||
},
|
||||
to_raylib(color));
|
||||
}
|
||||
|
||||
auto raylib_draw_context::stroke_rect(luma::rect bounds, std::float32_t thickness, luma::color color) -> void {
|
||||
assert(IsWindowReady());
|
||||
assert(thickness >= 0.0F);
|
||||
DrawRectangleLinesEx({
|
||||
.x = bounds.x,
|
||||
.y = bounds.y,
|
||||
.width = bounds.width,
|
||||
.height = bounds.height,
|
||||
},
|
||||
thickness, to_raylib(color));
|
||||
}
|
||||
|
||||
auto raylib_draw_context::stroke_line(luma::point from, luma::point to, std::float32_t thickness, luma::color color) -> void {
|
||||
assert(IsWindowReady());
|
||||
assert(thickness >= 0.0F);
|
||||
DrawLineEx({.x = from.x, .y = from.y}, {.x = to.x, .y = to.y}, thickness, to_raylib(color));
|
||||
}
|
||||
|
||||
auto raylib_draw_context::fill_ellipse(luma::rect bounds, luma::color color) -> void {
|
||||
assert(IsWindowReady());
|
||||
auto center_x = bounds.x + bounds.width * 0.5F;
|
||||
auto center_y = bounds.y + bounds.height * 0.5F;
|
||||
auto radius_x = std::max(std::float32_t{}, bounds.width * 0.5F);
|
||||
auto radius_y = std::max(std::float32_t{}, bounds.height * 0.5F);
|
||||
if (radius_x == 0.0F || radius_y == 0.0F) {
|
||||
return;
|
||||
}
|
||||
DrawEllipse(static_cast<std::int32_t>(std::lround(center_x)), static_cast<std::int32_t>(std::lround(center_y)), radius_x, radius_y, to_raylib(color));
|
||||
}
|
||||
|
||||
auto raylib_draw_context::stroke_ellipse(luma::rect bounds, std::float32_t thickness, luma::color color) -> void {
|
||||
assert(IsWindowReady());
|
||||
assert(thickness >= 0.0F);
|
||||
auto center_x = bounds.x + bounds.width * 0.5F;
|
||||
auto center_y = bounds.y + bounds.height * 0.5F;
|
||||
auto radius_x = std::max(std::float32_t{}, bounds.width * 0.5F);
|
||||
auto radius_y = std::max(std::float32_t{}, bounds.height * 0.5F);
|
||||
if (radius_x == 0.0F || radius_y == 0.0F || thickness == 0.0F) {
|
||||
return;
|
||||
}
|
||||
constexpr auto segments = std::int32_t{72};
|
||||
constexpr auto tau = std::float32_t{6.2831853071795864769F};
|
||||
auto previous = luma::point{.x = center_x + radius_x, .y = center_y};
|
||||
for (auto index = std::int32_t{1}; index <= segments; ++index) {
|
||||
auto angle = tau * static_cast<std::float32_t>(index) / static_cast<std::float32_t>(segments);
|
||||
auto current = luma::point{
|
||||
.x = center_x + std::cos(angle) * radius_x,
|
||||
.y = center_y + std::sin(angle) * radius_y,
|
||||
};
|
||||
DrawLineEx({.x = previous.x, .y = previous.y}, {.x = current.x, .y = current.y}, thickness, to_raylib(color));
|
||||
previous = current;
|
||||
}
|
||||
}
|
||||
|
||||
auto raylib_draw_context::draw_text(std::string_view text, luma::point origin, luma::text_style const &style, luma::color color) -> void {
|
||||
assert(IsWindowReady());
|
||||
assert(state_ != nullptr);
|
||||
assert(style.size > 0.0F);
|
||||
auto owned = std::string{text};
|
||||
auto font = resolve_font(*state_, style);
|
||||
DrawTextEx(font, owned.c_str(), {
|
||||
.x = origin.x,
|
||||
.y = origin.y,
|
||||
},
|
||||
style.size, 1.0F, to_raylib(color));
|
||||
}
|
||||
|
||||
auto raylib_draw_context::draw_texture(luma::texture const &texture, luma::rect source, luma::rect destination, luma::color tint) -> void {
|
||||
assert(IsWindowReady());
|
||||
auto const *native = dynamic_cast<raylib_texture const *>(&texture);
|
||||
if (native == nullptr || !native->valid()) {
|
||||
return;
|
||||
}
|
||||
DrawTexturePro(
|
||||
native->native(),
|
||||
{
|
||||
.x = source.x,
|
||||
.y = source.y,
|
||||
.width = source.width,
|
||||
.height = source.height,
|
||||
},
|
||||
{
|
||||
.x = destination.x,
|
||||
.y = destination.y,
|
||||
.width = destination.width,
|
||||
.height = destination.height,
|
||||
},
|
||||
{
|
||||
.x = 0.0F,
|
||||
.y = 0.0F,
|
||||
},
|
||||
0.0F,
|
||||
to_raylib(tint));
|
||||
}
|
||||
|
||||
auto raylib_draw_context::push_clip(luma::rect bounds) -> void {
|
||||
assert(IsWindowReady());
|
||||
auto clipped = clip_stack_.empty() ? bounds : intersect_clip(clip_stack_.back(), bounds);
|
||||
clip_stack_.push_back(clipped);
|
||||
begin_clip(clipped);
|
||||
}
|
||||
|
||||
auto raylib_draw_context::pop_clip() -> void {
|
||||
assert(IsWindowReady());
|
||||
assert(!clip_stack_.empty());
|
||||
EndScissorMode();
|
||||
clip_stack_.pop_back();
|
||||
if (!clip_stack_.empty()) {
|
||||
begin_clip(clip_stack_.back());
|
||||
}
|
||||
}
|
||||
|
||||
auto raylib_draw_context::measure_text(std::string_view text, luma::text_style const &style) const -> luma::size {
|
||||
assert(IsWindowReady());
|
||||
assert(state_ != nullptr);
|
||||
assert(style.size > 0.0F);
|
||||
auto owned = std::string{text};
|
||||
auto font = resolve_font(*state_, style);
|
||||
auto measured = MeasureTextEx(font, owned.c_str(), style.size, 1.0F);
|
||||
return {
|
||||
.width = measured.x,
|
||||
.height = style.line_height,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <raylib.h>
|
||||
#include <modulus.hpp>
|
||||
#include <luma/assets.hpp>
|
||||
#include <luma/geometry.hpp>
|
||||
#include <luma/input.hpp>
|
||||
#include <luma/render.hpp>
|
||||
|
||||
|
||||
namespace luma::detail {
|
||||
|
||||
struct font_key {
|
||||
std::string path{};
|
||||
std::int32_t size{};
|
||||
auto operator==(font_key const &other) const -> std::bool_t;
|
||||
};
|
||||
|
||||
struct font_key_hash {
|
||||
auto operator()(font_key const &key) const noexcept -> std::size_t;
|
||||
};
|
||||
|
||||
struct raylib_resource_state {
|
||||
std::thread::id ui_thread{};
|
||||
std::atomic_bool active{false};
|
||||
std::mutex mutex{};
|
||||
struct texture_record {
|
||||
Texture2D texture{};
|
||||
std::bool_t unloaded{false};
|
||||
};
|
||||
std::vector<std::weak_ptr<texture_record>> records{};
|
||||
std::vector<std::shared_ptr<texture_record>> pending_unloads{};
|
||||
std::unordered_map<font_key, Font, font_key_hash> fonts{};
|
||||
};
|
||||
|
||||
class raylib_texture final : public luma::texture {
|
||||
public:
|
||||
raylib_texture(std::shared_ptr<raylib_resource_state::texture_record> record, std::shared_ptr<raylib_resource_state> state);
|
||||
~raylib_texture() override;
|
||||
auto info() const -> luma::texture_info override;
|
||||
auto valid() const -> std::bool_t;
|
||||
auto native() const -> Texture2D;
|
||||
private:
|
||||
std::shared_ptr<raylib_resource_state::texture_record> record_{};
|
||||
std::shared_ptr<raylib_resource_state> state_{};
|
||||
};
|
||||
|
||||
class raylib_resources final : public luma::renderer_resources {
|
||||
public:
|
||||
explicit raylib_resources(std::shared_ptr<raylib_resource_state> state);
|
||||
auto load_texture(std::string const &path) -> std::expected<std::shared_ptr<luma::texture>, modulus::error> override;
|
||||
auto state() const -> std::shared_ptr<raylib_resource_state>;
|
||||
private:
|
||||
std::shared_ptr<raylib_resource_state> state_{};
|
||||
};
|
||||
|
||||
class raylib_draw_context final : public luma::draw_context {
|
||||
public:
|
||||
explicit raylib_draw_context(std::shared_ptr<raylib_resource_state> state);
|
||||
auto fill_rect(luma::rect bounds, luma::color color) -> void override;
|
||||
auto stroke_rect(luma::rect bounds, std::float32_t thickness, luma::color color) -> void override;
|
||||
auto stroke_line(luma::point from, luma::point to, std::float32_t thickness, luma::color color) -> void override;
|
||||
auto fill_ellipse(luma::rect bounds, luma::color color) -> void override;
|
||||
auto stroke_ellipse(luma::rect bounds, std::float32_t thickness, luma::color color) -> void override;
|
||||
auto draw_text(std::string_view text, luma::point origin, luma::text_style const &style, luma::color color) -> void override;
|
||||
auto draw_texture(luma::texture const &texture, luma::rect source, luma::rect destination, luma::color tint) -> void override;
|
||||
auto push_clip(luma::rect bounds) -> void override;
|
||||
auto pop_clip() -> void override;
|
||||
auto measure_text(std::string_view text, luma::text_style const &style) const -> luma::size override;
|
||||
private:
|
||||
std::shared_ptr<raylib_resource_state> state_{};
|
||||
std::vector<luma::rect> clip_stack_{};
|
||||
};
|
||||
|
||||
class drawing_scope {
|
||||
public:
|
||||
drawing_scope();
|
||||
drawing_scope(drawing_scope const &) = delete;
|
||||
auto operator=(drawing_scope const &) -> drawing_scope & = delete;
|
||||
~drawing_scope();
|
||||
auto end() -> void;
|
||||
private:
|
||||
std::bool_t active_{true};
|
||||
};
|
||||
|
||||
auto to_raylib(luma::color color) -> Color;
|
||||
auto flush_pending_unloads(raylib_resource_state &state) -> std::bool_t;
|
||||
auto unload_all_textures(raylib_resource_state &state) -> void;
|
||||
auto unload_all_fonts(raylib_resource_state &state) -> void;
|
||||
auto poll_touch(std::unordered_map<std::int32_t, luma::point> &previous, std::int32_t max_points) -> luma::touch_event;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
#include "detail/control_types.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
|
||||
namespace luma::detail {
|
||||
|
||||
panel_control::panel_control(std::vector<std::unique_ptr<luma::control>> children)
|
||||
: children_{std::move(children)} {}
|
||||
|
||||
auto panel_control::measure(luma::draw_context &context, luma::theme const &theme, luma::constraints limits) -> luma::size {
|
||||
assert(finite_constraints(limits));
|
||||
auto child_limits = luma::deflate(limits, panel_padding);
|
||||
auto result = child_limits.min;
|
||||
for (auto &child : children_) {
|
||||
assert(child != nullptr);
|
||||
auto measured = child->measure(context, theme, child_limits);
|
||||
result.width = std::max(result.width, measured.width);
|
||||
result.height = std::max(result.height, measured.height);
|
||||
}
|
||||
result.width += panel_padding.left + panel_padding.right;
|
||||
result.height += panel_padding.top + panel_padding.bottom;
|
||||
return luma::clamp(result, limits);
|
||||
}
|
||||
|
||||
auto panel_control::arrange(luma::rect bounds) -> void {
|
||||
assert(finite_rect(bounds));
|
||||
luma::control::arrange(bounds);
|
||||
auto content = luma::inset(bounds, panel_padding);
|
||||
for (auto &child : children_) {
|
||||
assert(child != nullptr);
|
||||
child->arrange(content);
|
||||
}
|
||||
}
|
||||
|
||||
auto panel_control::draw(luma::draw_context &context, luma::theme const &theme) const -> void {
|
||||
context.fill_rect(bounds(), theme.surface);
|
||||
for (auto const &child : children_) {
|
||||
assert(child != nullptr);
|
||||
child->draw(context, theme);
|
||||
}
|
||||
}
|
||||
|
||||
auto panel_control::handle_touch(luma::touch_event const &event) -> std::bool_t {
|
||||
return dispatch_to_children(children_, event);
|
||||
}
|
||||
|
||||
auto panel_control::invalidate_layout() -> void {
|
||||
for (auto &child : children_) {
|
||||
assert(child != nullptr);
|
||||
child->invalidate_layout();
|
||||
}
|
||||
}
|
||||
|
||||
auto panel_control::bind_layout_invalidator(std::function<void()> invalidator) -> void {
|
||||
luma::control::bind_layout_invalidator(invalidator);
|
||||
for (auto &child : children_) {
|
||||
assert(child != nullptr);
|
||||
child->bind_layout_invalidator(invalidator);
|
||||
}
|
||||
}
|
||||
|
||||
linear_control::linear_control(luma::direction direction, std::vector<std::unique_ptr<luma::control>> children, std::float32_t gap, luma::alignment align)
|
||||
: direction_{direction}, children_{std::move(children)}, gap_{gap}, align_{align} {
|
||||
assert(std::isfinite(gap_));
|
||||
assert(gap_ >= 0.0F);
|
||||
}
|
||||
|
||||
auto linear_control::measure(luma::draw_context &context, luma::theme const &theme, luma::constraints limits) -> luma::size {
|
||||
assert(finite_constraints(limits));
|
||||
measured_.clear();
|
||||
measured_.reserve(children_.size());
|
||||
auto result = luma::size{};
|
||||
for (auto index = std::size_t{}; index < children_.size(); ++index) {
|
||||
auto &child = children_[index];
|
||||
assert(child != nullptr);
|
||||
auto child_limits = limits;
|
||||
if (direction_ == luma::direction::ROW) {
|
||||
auto used = result.width + (index == 0 ? 0.0F : gap_);
|
||||
child_limits.min.width = 0.0F;
|
||||
child_limits.max.width = std::max(std::float32_t{}, limits.max.width - used);
|
||||
} else {
|
||||
auto used = result.height + (index == 0 ? 0.0F : gap_);
|
||||
child_limits.min.height = 0.0F;
|
||||
child_limits.max.height = std::max(std::float32_t{}, limits.max.height - used);
|
||||
}
|
||||
auto measured = child->measure(context, theme, child_limits);
|
||||
measured_.push_back(measured);
|
||||
if (direction_ == luma::direction::ROW) {
|
||||
result.width += measured.width + (index == 0 ? 0.0F : gap_);
|
||||
result.height = std::max(result.height, measured.height);
|
||||
} else {
|
||||
result.width = std::max(result.width, measured.width);
|
||||
result.height += measured.height + (index == 0 ? 0.0F : gap_);
|
||||
}
|
||||
}
|
||||
return luma::clamp(result, limits);
|
||||
}
|
||||
|
||||
auto linear_control::arrange(luma::rect bounds) -> void {
|
||||
assert(finite_rect(bounds));
|
||||
luma::control::arrange(bounds);
|
||||
auto cursor = direction_ == luma::direction::ROW ? bounds.x : bounds.y;
|
||||
for (auto index = std::size_t{}; index < children_.size(); ++index) {
|
||||
auto &child = children_[index];
|
||||
assert(child != nullptr);
|
||||
auto measured = index < measured_.size() ? measured_[index] : luma::size{};
|
||||
auto child_bounds = bounds;
|
||||
if (direction_ == luma::direction::ROW) {
|
||||
child_bounds.x = cursor;
|
||||
child_bounds.width = std::min(measured.width, std::max(std::float32_t{}, luma::right(bounds) - cursor));
|
||||
child_bounds.height = align_ == luma::alignment::STRETCH ? bounds.height : std::min(measured.height, bounds.height);
|
||||
if (align_ == luma::alignment::CENTER) {
|
||||
child_bounds.y = bounds.y + (bounds.height - child_bounds.height) * 0.5F;
|
||||
} else if (align_ == luma::alignment::END) {
|
||||
child_bounds.y = luma::bottom(bounds) - child_bounds.height;
|
||||
}
|
||||
cursor += child_bounds.width + gap_;
|
||||
} else {
|
||||
child_bounds.y = cursor;
|
||||
child_bounds.width = align_ == luma::alignment::STRETCH ? bounds.width : std::min(measured.width, bounds.width);
|
||||
child_bounds.height = std::min(measured.height, std::max(std::float32_t{}, luma::bottom(bounds) - cursor));
|
||||
if (align_ == luma::alignment::CENTER) {
|
||||
child_bounds.x = bounds.x + (bounds.width - child_bounds.width) * 0.5F;
|
||||
} else if (align_ == luma::alignment::END) {
|
||||
child_bounds.x = luma::right(bounds) - child_bounds.width;
|
||||
}
|
||||
cursor += child_bounds.height + gap_;
|
||||
}
|
||||
child->arrange(child_bounds);
|
||||
}
|
||||
}
|
||||
|
||||
auto linear_control::draw(luma::draw_context &context, luma::theme const &theme) const -> void {
|
||||
for (auto const &child : children_) {
|
||||
assert(child != nullptr);
|
||||
child->draw(context, theme);
|
||||
}
|
||||
}
|
||||
|
||||
auto linear_control::handle_touch(luma::touch_event const &event) -> std::bool_t {
|
||||
return dispatch_to_children(children_, event);
|
||||
}
|
||||
|
||||
auto linear_control::invalidate_layout() -> void {
|
||||
measured_.clear();
|
||||
for (auto &child : children_) {
|
||||
assert(child != nullptr);
|
||||
child->invalidate_layout();
|
||||
}
|
||||
}
|
||||
|
||||
auto linear_control::bind_layout_invalidator(std::function<void()> invalidator) -> void {
|
||||
luma::control::bind_layout_invalidator(invalidator);
|
||||
for (auto &child : children_) {
|
||||
assert(child != nullptr);
|
||||
child->bind_layout_invalidator(invalidator);
|
||||
}
|
||||
}
|
||||
|
||||
stack_control::stack_control(std::vector<std::unique_ptr<luma::control>> children)
|
||||
: children_{std::move(children)} {}
|
||||
|
||||
auto stack_control::measure(luma::draw_context &context, luma::theme const &theme, luma::constraints limits) -> luma::size {
|
||||
assert(finite_constraints(limits));
|
||||
auto result = limits.min;
|
||||
for (auto &child : children_) {
|
||||
assert(child != nullptr);
|
||||
auto measured = child->measure(context, theme, limits);
|
||||
result.width = std::max(result.width, measured.width);
|
||||
result.height = std::max(result.height, measured.height);
|
||||
}
|
||||
return luma::clamp(result, limits);
|
||||
}
|
||||
|
||||
auto stack_control::arrange(luma::rect bounds) -> void {
|
||||
assert(finite_rect(bounds));
|
||||
luma::control::arrange(bounds);
|
||||
for (auto &child : children_) {
|
||||
assert(child != nullptr);
|
||||
child->arrange(bounds);
|
||||
}
|
||||
}
|
||||
|
||||
auto stack_control::draw(luma::draw_context &context, luma::theme const &theme) const -> void {
|
||||
for (auto const &child : children_) {
|
||||
assert(child != nullptr);
|
||||
child->draw(context, theme);
|
||||
}
|
||||
}
|
||||
|
||||
auto stack_control::handle_touch(luma::touch_event const &event) -> std::bool_t {
|
||||
return dispatch_to_children(children_, event);
|
||||
}
|
||||
|
||||
auto stack_control::invalidate_layout() -> void {
|
||||
for (auto &child : children_) {
|
||||
assert(child != nullptr);
|
||||
child->invalidate_layout();
|
||||
}
|
||||
}
|
||||
|
||||
auto stack_control::bind_layout_invalidator(std::function<void()> invalidator) -> void {
|
||||
luma::control::bind_layout_invalidator(invalidator);
|
||||
for (auto &child : children_) {
|
||||
assert(child != nullptr);
|
||||
child->bind_layout_invalidator(invalidator);
|
||||
}
|
||||
}
|
||||
|
||||
scroll_view_control::scroll_view_control(std::unique_ptr<luma::control> child, std::bool_t vertical, std::bool_t horizontal)
|
||||
: child_{std::move(child)}, vertical_{vertical}, horizontal_{horizontal} {
|
||||
assert(child_ != nullptr);
|
||||
assert(vertical_ || horizontal_);
|
||||
}
|
||||
|
||||
auto scroll_view_control::measure(luma::draw_context &context, luma::theme const &theme, luma::constraints limits) -> luma::size {
|
||||
assert(finite_constraints(limits));
|
||||
auto max = std::numeric_limits<std::float32_t>::max();
|
||||
auto child_limits = luma::constraints{
|
||||
.min = {},
|
||||
.max = {
|
||||
.width = horizontal_ ? max : limits.max.width,
|
||||
.height = vertical_ ? max : limits.max.height,
|
||||
},
|
||||
};
|
||||
if (content_measure_valid_ && cached_child_limits_ && cached_theme_ && same_constraints(*cached_child_limits_, child_limits) && same_theme_metrics(*cached_theme_, theme)) {
|
||||
content_size_ = cached_content_size_;
|
||||
return luma::clamp(content_size_, limits);
|
||||
}
|
||||
content_size_ = child_->measure(context, theme, child_limits);
|
||||
cached_child_limits_ = child_limits;
|
||||
cached_theme_ = theme;
|
||||
cached_content_size_ = content_size_;
|
||||
content_measure_valid_ = true;
|
||||
return luma::clamp(content_size_, limits);
|
||||
}
|
||||
|
||||
auto scroll_view_control::arrange(luma::rect bounds) -> void {
|
||||
assert(finite_rect(bounds));
|
||||
luma::control::arrange(bounds);
|
||||
clamp_offset();
|
||||
arrange_child();
|
||||
}
|
||||
|
||||
auto scroll_view_control::draw(luma::draw_context &context, luma::theme const &theme) const -> void {
|
||||
context.push_clip(bounds());
|
||||
child_->draw(context, theme);
|
||||
context.pop_clip();
|
||||
}
|
||||
|
||||
auto scroll_view_control::handle_touch(luma::touch_event const &event) -> std::bool_t {
|
||||
auto handled = false;
|
||||
for (auto const &point : event.points) {
|
||||
handled = handle_point(event, point) || handled;
|
||||
}
|
||||
return handled;
|
||||
}
|
||||
|
||||
auto scroll_view_control::handle_point(luma::touch_event const &event, luma::touch_point const &point) -> std::bool_t {
|
||||
if (point.phase == luma::touch_phase::DOWN) {
|
||||
if (!luma::contains(bounds(), point.position)) {
|
||||
return false;
|
||||
}
|
||||
auto child_active = child_->handle_touch(make_event(event, point));
|
||||
if (child_active) {
|
||||
invalidate_child_measure();
|
||||
}
|
||||
auto state = scroll_view_control::touch_state{
|
||||
.start = point.position,
|
||||
.last = point.position,
|
||||
.child_active = child_active,
|
||||
};
|
||||
touches_[point.id] = state;
|
||||
if (!scroll_id_) {
|
||||
dragging_ = false;
|
||||
velocity_ = {};
|
||||
}
|
||||
return true;
|
||||
}
|
||||
auto found = touches_.find(point.id);
|
||||
if (found == touches_.end()) {
|
||||
return false;
|
||||
}
|
||||
auto &state = found->second;
|
||||
if (point.phase == luma::touch_phase::MOVE) {
|
||||
auto delta = luma::point{
|
||||
.x = point.position.x - state.last.x,
|
||||
.y = point.position.y - state.last.y,
|
||||
};
|
||||
if (!scroll_id_) {
|
||||
auto total = luma::point{
|
||||
.x = point.position.x - state.start.x,
|
||||
.y = point.position.y - state.start.y,
|
||||
};
|
||||
auto moved = std::float32_t{};
|
||||
if (horizontal_) {
|
||||
moved = std::max(moved, std::abs(total.x));
|
||||
}
|
||||
if (vertical_) {
|
||||
moved = std::max(moved, std::abs(total.y));
|
||||
}
|
||||
if (moved >= scroll_drag_threshold) {
|
||||
scroll_id_ = point.id;
|
||||
dragging_ = true;
|
||||
if (state.child_active) {
|
||||
auto cancel = point;
|
||||
cancel.phase = luma::touch_phase::CANCEL;
|
||||
static_cast<void>(child_->handle_touch(make_event(event, cancel)));
|
||||
state.child_active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (scroll_id_ == point.id && dragging_) {
|
||||
if (horizontal_) {
|
||||
offset_.x -= delta.x;
|
||||
velocity_.x = delta.x;
|
||||
}
|
||||
if (vertical_) {
|
||||
offset_.y -= delta.y;
|
||||
velocity_.y = delta.y;
|
||||
}
|
||||
state.last = point.position;
|
||||
clamp_offset();
|
||||
arrange_child();
|
||||
return true;
|
||||
}
|
||||
state.last = point.position;
|
||||
if (state.child_active) {
|
||||
state.child_active = child_->handle_touch(make_event(event, point));
|
||||
invalidate_child_measure();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (point.phase == luma::touch_phase::UP || point.phase == luma::touch_phase::CANCEL) {
|
||||
auto const scrolling = scroll_id_ == point.id && dragging_;
|
||||
if (state.child_active) {
|
||||
if (scrolling) {
|
||||
auto cancel = point;
|
||||
cancel.phase = luma::touch_phase::CANCEL;
|
||||
static_cast<void>(child_->handle_touch(make_event(event, cancel)));
|
||||
} else {
|
||||
static_cast<void>(child_->handle_touch(make_event(event, point)));
|
||||
}
|
||||
invalidate_child_measure();
|
||||
}
|
||||
if (scroll_id_ == point.id) {
|
||||
scroll_id_.reset();
|
||||
dragging_ = false;
|
||||
velocity_ = {};
|
||||
}
|
||||
touches_.erase(found);
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
auto scroll_view_control::invalidate_layout() -> void {
|
||||
invalidate_child_measure();
|
||||
}
|
||||
|
||||
auto scroll_view_control::bind_layout_invalidator(std::function<void()> invalidator) -> void {
|
||||
luma::control::bind_layout_invalidator(invalidator);
|
||||
child_->bind_layout_invalidator(std::move(invalidator));
|
||||
}
|
||||
|
||||
auto scroll_view_control::invalidate_child_measure() -> void {
|
||||
content_measure_valid_ = false;
|
||||
cached_child_limits_.reset();
|
||||
cached_theme_.reset();
|
||||
cached_content_size_ = {};
|
||||
child_->invalidate_layout();
|
||||
}
|
||||
|
||||
auto scroll_view_control::arrange_child() -> void {
|
||||
auto area = bounds();
|
||||
child_->arrange({
|
||||
.x = area.x - offset_.x,
|
||||
.y = area.y - offset_.y,
|
||||
.width = std::max(area.width, content_size_.width),
|
||||
.height = std::max(area.height, content_size_.height),
|
||||
});
|
||||
}
|
||||
|
||||
auto scroll_view_control::clamp_offset() -> void {
|
||||
offset_.x = horizontal_ ? std::clamp(offset_.x, std::float32_t{}, std::max(std::float32_t{}, content_size_.width - bounds().width)) : std::float32_t{};
|
||||
offset_.y = vertical_ ? std::clamp(offset_.y, std::float32_t{}, std::max(std::float32_t{}, content_size_.height - bounds().height)) : std::float32_t{};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
#include "detail/control_types.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
|
||||
namespace luma::detail {
|
||||
|
||||
text_field_control::text_field_control(std::shared_ptr<luma::text_input_state> input)
|
||||
: input_{std::move(input)} {
|
||||
assert(input_ != nullptr);
|
||||
}
|
||||
|
||||
auto text_field_control::measure(luma::draw_context &context, luma::theme const &theme, luma::constraints limits) -> luma::size {
|
||||
assert(finite_constraints(limits));
|
||||
auto version = input_->version();
|
||||
if (cached_limits_ && cached_style_ && same_constraints(*cached_limits_, limits) && same_text_style(*cached_style_, theme.body) && cached_version_ == version) {
|
||||
return cached_size_;
|
||||
}
|
||||
auto options = input_->snapshot();
|
||||
auto text = options.text.empty() ? std::string{"Text"} : options.text;
|
||||
auto measured = context.measure_text(text, theme.body);
|
||||
cached_limits_ = limits;
|
||||
cached_style_ = theme.body;
|
||||
cached_version_ = version;
|
||||
cached_size_ = luma::clamp({
|
||||
.width = std::max(std::float32_t{240}, measured.width + std::float32_t{24}),
|
||||
.height = std::max(std::float32_t{48}, measured.height + std::float32_t{16}),
|
||||
},
|
||||
limits);
|
||||
return cached_size_;
|
||||
}
|
||||
|
||||
auto text_field_control::draw(luma::draw_context &context, luma::theme const &theme) const -> void {
|
||||
context.fill_rect(bounds(), theme.surface);
|
||||
context.stroke_rect(bounds(), 1.0F, input_->focused() ? theme.accent : theme.border);
|
||||
auto options = input_->snapshot();
|
||||
auto text = options.text;
|
||||
if (text.empty()) {
|
||||
text = options.placeholder;
|
||||
} else if (options.password) {
|
||||
text = std::string(options.text.size(), '*');
|
||||
}
|
||||
context.draw_text(text, {
|
||||
.x = bounds().x + 12.0F,
|
||||
.y = bounds().y + 8.0F,
|
||||
},
|
||||
theme.body, options.text.empty() ? theme.text_muted : theme.text);
|
||||
}
|
||||
|
||||
auto text_field_control::handle_touch(luma::touch_event const &event) -> std::bool_t {
|
||||
if (!enabled()) {
|
||||
return false;
|
||||
}
|
||||
auto const *point = first_point_inside(event, bounds(), luma::touch_phase::DOWN);
|
||||
if (point == nullptr) {
|
||||
point = first_point_with_phase(event, luma::touch_phase::DOWN);
|
||||
}
|
||||
if (point == nullptr) {
|
||||
return false;
|
||||
}
|
||||
auto hit = luma::contains(bounds(), point->position);
|
||||
input_->set_focused(hit);
|
||||
set_focused(hit);
|
||||
return hit;
|
||||
}
|
||||
|
||||
auto text_field_control::invalidate_layout() -> void {
|
||||
cached_limits_.reset();
|
||||
cached_style_.reset();
|
||||
cached_version_ = {};
|
||||
cached_size_ = {};
|
||||
}
|
||||
|
||||
auto text_field_control::bind_layout_invalidator(std::function<void()> invalidator) -> void {
|
||||
luma::control::bind_layout_invalidator(invalidator);
|
||||
if (!invalidator) {
|
||||
layout_listener_lifetime_.reset();
|
||||
return;
|
||||
}
|
||||
layout_listener_lifetime_ = std::make_shared<std::int32_t>();
|
||||
input_->add_layout_listener(layout_listener_lifetime_, [invalidator = std::move(invalidator)] {
|
||||
invalidator();
|
||||
});
|
||||
}
|
||||
|
||||
auto text_field_control::focusable() const -> std::bool_t {
|
||||
return true;
|
||||
}
|
||||
|
||||
soft_keyboard_control::soft_keyboard_control(luma::keyboard_layout layout, std::function<void(std::string)> on_text, std::function<void()> on_enter)
|
||||
: layout_{std::move(layout)}, on_text_{std::move(on_text)}, on_enter_{std::move(on_enter)} {
|
||||
assert(!layout_.rows.empty());
|
||||
assert(on_text_);
|
||||
}
|
||||
|
||||
soft_keyboard_control::soft_keyboard_control(luma::keyboard_layout layout, std::shared_ptr<luma::text_input_state> input, std::function<void()> on_enter)
|
||||
: layout_{std::move(layout)}, on_enter_{std::move(on_enter)}, input_{std::move(input)} {
|
||||
assert(!layout_.rows.empty());
|
||||
assert(input_ != nullptr);
|
||||
}
|
||||
|
||||
auto soft_keyboard_control::measure(luma::draw_context &, luma::theme const &, luma::constraints limits) -> luma::size {
|
||||
assert(finite_constraints(limits));
|
||||
return luma::clamp({
|
||||
.width = 640.0F,
|
||||
.height = static_cast<std::float32_t>(layout_.rows.size()) * 56.0F,
|
||||
},
|
||||
limits);
|
||||
}
|
||||
|
||||
auto soft_keyboard_control::draw(luma::draw_context &context, luma::theme const &theme) const -> void {
|
||||
auto key_height = bounds().height / static_cast<std::float32_t>(layout_.rows.size());
|
||||
for (auto row_index = std::size_t{}; row_index < layout_.rows.size(); ++row_index) {
|
||||
auto const &row = layout_.rows[row_index];
|
||||
if (row.empty()) {
|
||||
continue;
|
||||
}
|
||||
auto key_width = bounds().width / static_cast<std::float32_t>(row.size());
|
||||
for (auto column_index = std::size_t{}; column_index < row.size(); ++column_index) {
|
||||
auto rect = luma::rect{
|
||||
.x = bounds().x + key_width * static_cast<std::float32_t>(column_index),
|
||||
.y = bounds().y + key_height * static_cast<std::float32_t>(row_index),
|
||||
.width = key_width - 4.0F,
|
||||
.height = key_height - 4.0F,
|
||||
};
|
||||
context.fill_rect(rect, theme.surface);
|
||||
context.stroke_rect(rect, 1.0F, theme.border);
|
||||
context.draw_text(row[column_index].label, {
|
||||
.x = rect.x + 12.0F,
|
||||
.y = rect.y + 10.0F,
|
||||
},
|
||||
theme.body, theme.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto soft_keyboard_control::handle_touch(luma::touch_event const &event) -> std::bool_t {
|
||||
auto const *point = first_point_inside(event, bounds(), luma::touch_phase::UP);
|
||||
if (point == nullptr) {
|
||||
return false;
|
||||
}
|
||||
auto row_count = layout_.rows.size();
|
||||
assert(row_count > 0);
|
||||
auto row_index = std::min(row_count - 1, static_cast<std::size_t>((point->position.y - bounds().y) / (bounds().height / static_cast<std::float32_t>(row_count))));
|
||||
auto const &row = layout_.rows[row_index];
|
||||
if (row.empty()) {
|
||||
return true;
|
||||
}
|
||||
auto column_index = std::min(row.size() - 1, static_cast<std::size_t>((point->position.x - bounds().x) / (bounds().width / static_cast<std::float32_t>(row.size()))));
|
||||
auto const &key = row[column_index];
|
||||
switch (key.kind) {
|
||||
case luma::keyboard_key_kind::CHARACTER:
|
||||
if (input_) {
|
||||
throw_if_failed(input_->append(key.value.empty() ? key.label : key.value));
|
||||
} else {
|
||||
assert(on_text_);
|
||||
on_text_(key.value.empty() ? key.label : key.value);
|
||||
}
|
||||
break;
|
||||
case luma::keyboard_key_kind::BACKSPACE:
|
||||
if (input_) {
|
||||
throw_if_failed(input_->backspace());
|
||||
} else {
|
||||
assert(on_text_);
|
||||
on_text_("\b");
|
||||
}
|
||||
break;
|
||||
case luma::keyboard_key_kind::SPACE:
|
||||
if (input_) {
|
||||
throw_if_failed(input_->append(" "));
|
||||
} else {
|
||||
assert(on_text_);
|
||||
on_text_(" ");
|
||||
}
|
||||
break;
|
||||
case luma::keyboard_key_kind::ENTER:
|
||||
if (on_enter_) {
|
||||
on_enter_();
|
||||
}
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
#include <luma/controls.hpp>
|
||||
|
||||
#include <exception>
|
||||
#include <mutex>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <modulus.hpp>
|
||||
|
||||
#include <luma/detail/errors.hpp>
|
||||
#include <luma/geometry.hpp>
|
||||
#include <luma/input.hpp>
|
||||
#include <luma/render.hpp>
|
||||
#include <luma/theme.hpp>
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
auto accepts_text(luma::text_input_mode mode, std::string_view current, std::string_view value) -> std::bool_t {
|
||||
if (mode == luma::text_input_mode::TEXT) {
|
||||
return true;
|
||||
}
|
||||
auto decimal_seen = current.find('.') != std::string::npos;
|
||||
for (auto ch : value) {
|
||||
if (ch >= '0' && ch <= '9') {
|
||||
continue;
|
||||
}
|
||||
if (mode == luma::text_input_mode::DECIMAL && ch == '.' && !decimal_seen) {
|
||||
decimal_seen = true;
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
auto validate_text_options(luma::text_field_options const &options) -> std::expected<void, modulus::error> {
|
||||
if (options.max_length == 0) {
|
||||
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "text field max length must be positive");
|
||||
}
|
||||
if (options.text.size() > options.max_length) {
|
||||
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "text field initial text exceeds max length");
|
||||
}
|
||||
if (!accepts_text(options.mode, "", options.text)) {
|
||||
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "text field initial text does not match input mode");
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto callback_failure(std::string message) -> std::expected<void, modulus::error> {
|
||||
return luma::detail::fail<void>(luma::detail::error_internal, std::move(message));
|
||||
}
|
||||
|
||||
auto notify_text_changed(std::function<void(std::string)> const &callback, std::string value) -> std::expected<void, modulus::error> {
|
||||
if (!callback) {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
callback(std::move(value));
|
||||
} catch (std::exception const &error) {
|
||||
return callback_failure(std::string{"text change callback failed: "} + error.what());
|
||||
} catch (...) {
|
||||
return callback_failure("text change callback failed with an unknown exception");
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto notify_layout_listeners(std::vector<std::function<void()>> const &listeners) -> void {
|
||||
for (auto const &listener : listeners) {
|
||||
if (!listener) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
listener();
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto collect_layout_listeners(std::vector<luma::text_input_state::layout_listener> &listeners) -> std::vector<std::function<void()>> {
|
||||
auto callbacks = std::vector<std::function<void()>>{};
|
||||
auto write = listeners.begin();
|
||||
for (auto read = listeners.begin(); read != listeners.end(); ++read) {
|
||||
if (read->lifetime.expired()) {
|
||||
continue;
|
||||
}
|
||||
callbacks.push_back(read->callback);
|
||||
if (write != read) {
|
||||
*write = std::move(*read);
|
||||
}
|
||||
++write;
|
||||
}
|
||||
listeners.erase(write, listeners.end());
|
||||
return callbacks;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
luma::text_input_state::text_input_state(luma::text_field_options options, std::function<void(std::string)> on_change)
|
||||
: options_{std::move(options)}, on_change_{std::move(on_change)} {
|
||||
assert(options_.max_length > 0);
|
||||
assert(on_change_);
|
||||
}
|
||||
|
||||
auto luma::text_input_state::snapshot() const -> luma::text_field_options {
|
||||
auto lock = std::scoped_lock{mutex_};
|
||||
return options_;
|
||||
}
|
||||
|
||||
auto luma::text_input_state::text() const -> std::string {
|
||||
auto lock = std::scoped_lock{mutex_};
|
||||
return options_.text;
|
||||
}
|
||||
|
||||
auto luma::text_input_state::focused() const -> std::bool_t {
|
||||
auto lock = std::scoped_lock{mutex_};
|
||||
return focused_;
|
||||
}
|
||||
|
||||
auto luma::text_input_state::set_focused(std::bool_t value) -> void {
|
||||
auto lock = std::scoped_lock{mutex_};
|
||||
focused_ = value;
|
||||
}
|
||||
|
||||
auto luma::text_input_state::version() const -> std::uint64_t {
|
||||
auto lock = std::scoped_lock{mutex_};
|
||||
return version_;
|
||||
}
|
||||
|
||||
auto luma::text_input_state::add_layout_listener(std::weak_ptr<void> lifetime, std::function<void()> callback) -> void {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
auto lock = std::scoped_lock{mutex_};
|
||||
layout_listeners_.push_back({
|
||||
.lifetime = std::move(lifetime),
|
||||
.callback = std::move(callback),
|
||||
});
|
||||
}
|
||||
|
||||
auto luma::text_input_state::append(std::string_view value) -> std::expected<void, modulus::error> {
|
||||
if (value == "\b") {
|
||||
return backspace();
|
||||
}
|
||||
auto callback_text = std::string{};
|
||||
auto layout_listeners = std::vector<std::function<void()>>{};
|
||||
auto changed = false;
|
||||
{
|
||||
auto lock = std::scoped_lock{mutex_};
|
||||
if (value.empty()) {
|
||||
return {};
|
||||
}
|
||||
if (options_.text.size() + value.size() > options_.max_length) {
|
||||
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "text field max length exceeded");
|
||||
}
|
||||
if (!accepts_text(options_.mode, options_.text, value)) {
|
||||
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "text does not match input mode");
|
||||
}
|
||||
options_.text.append(value.data(), value.size());
|
||||
++version_;
|
||||
callback_text = options_.text;
|
||||
layout_listeners = collect_layout_listeners(layout_listeners_);
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
auto status = notify_text_changed(on_change_, std::move(callback_text));
|
||||
notify_layout_listeners(layout_listeners);
|
||||
return status;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto luma::text_input_state::backspace() -> std::expected<void, modulus::error> {
|
||||
auto callback_text = std::string{};
|
||||
auto layout_listeners = std::vector<std::function<void()>>{};
|
||||
auto changed = false;
|
||||
{
|
||||
auto lock = std::scoped_lock{mutex_};
|
||||
if (options_.text.empty()) {
|
||||
return {};
|
||||
}
|
||||
options_.text.pop_back();
|
||||
++version_;
|
||||
callback_text = options_.text;
|
||||
layout_listeners = collect_layout_listeners(layout_listeners_);
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
auto status = notify_text_changed(on_change_, std::move(callback_text));
|
||||
notify_layout_listeners(layout_listeners);
|
||||
return status;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto luma::text_input_state::set_text(std::string value) -> std::expected<void, modulus::error> {
|
||||
auto callback_text = std::string{};
|
||||
auto layout_listeners = std::vector<std::function<void()>>{};
|
||||
{
|
||||
auto lock = std::scoped_lock{mutex_};
|
||||
if (value.size() > options_.max_length) {
|
||||
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "text field max length exceeded");
|
||||
}
|
||||
if (!accepts_text(options_.mode, "", value)) {
|
||||
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "text does not match input mode");
|
||||
}
|
||||
options_.text = std::move(value);
|
||||
++version_;
|
||||
callback_text = options_.text;
|
||||
layout_listeners = collect_layout_listeners(layout_listeners_);
|
||||
}
|
||||
auto status = notify_text_changed(on_change_, std::move(callback_text));
|
||||
notify_layout_listeners(layout_listeners);
|
||||
return status;
|
||||
}
|
||||
|
||||
auto luma::make_text_input(luma::text_field_options options, std::function<void(std::string)> on_change) -> std::expected<std::shared_ptr<luma::text_input_state>, modulus::error> {
|
||||
if (!on_change) {
|
||||
return luma::detail::fail<std::shared_ptr<luma::text_input_state>>(luma::detail::error_invalid_argument, "text field change callback must not be empty");
|
||||
}
|
||||
if (auto status = validate_text_options(options); !status) {
|
||||
return modulus::fail<std::shared_ptr<luma::text_input_state>>(status.error());
|
||||
}
|
||||
return modulus::success(std::make_shared<luma::text_input_state>(std::move(options), std::move(on_change)));
|
||||
}
|
||||
|
||||
auto luma::default_text_keyboard() -> luma::keyboard_layout {
|
||||
return {
|
||||
.rows = {
|
||||
{
|
||||
{
|
||||
"Q",
|
||||
"q",
|
||||
},
|
||||
{
|
||||
"W",
|
||||
"w",
|
||||
},
|
||||
{
|
||||
"E",
|
||||
"e",
|
||||
},
|
||||
{
|
||||
"R",
|
||||
"r",
|
||||
},
|
||||
{
|
||||
"T",
|
||||
"t",
|
||||
},
|
||||
{
|
||||
"Y",
|
||||
"y",
|
||||
},
|
||||
{
|
||||
"U",
|
||||
"u",
|
||||
},
|
||||
{
|
||||
"I",
|
||||
"i",
|
||||
},
|
||||
{
|
||||
"O",
|
||||
"o",
|
||||
},
|
||||
{
|
||||
"P",
|
||||
"p",
|
||||
},
|
||||
},
|
||||
{
|
||||
{
|
||||
"A",
|
||||
"a",
|
||||
},
|
||||
{
|
||||
"S",
|
||||
"s",
|
||||
},
|
||||
{
|
||||
"D",
|
||||
"d",
|
||||
},
|
||||
{
|
||||
"F",
|
||||
"f",
|
||||
},
|
||||
{
|
||||
"G",
|
||||
"g",
|
||||
},
|
||||
{
|
||||
"H",
|
||||
"h",
|
||||
},
|
||||
{
|
||||
"J",
|
||||
"j",
|
||||
},
|
||||
{
|
||||
"K",
|
||||
"k",
|
||||
},
|
||||
{
|
||||
"L",
|
||||
"l",
|
||||
},
|
||||
},
|
||||
{
|
||||
{
|
||||
"Z",
|
||||
"z",
|
||||
},
|
||||
{
|
||||
"X",
|
||||
"x",
|
||||
},
|
||||
{
|
||||
"C",
|
||||
"c",
|
||||
},
|
||||
{
|
||||
"V",
|
||||
"v",
|
||||
},
|
||||
{
|
||||
"B",
|
||||
"b",
|
||||
},
|
||||
{
|
||||
"N",
|
||||
"n",
|
||||
},
|
||||
{
|
||||
"M",
|
||||
"m",
|
||||
},
|
||||
{
|
||||
"<-",
|
||||
"",
|
||||
luma::keyboard_key_kind::BACKSPACE,
|
||||
},
|
||||
},
|
||||
{
|
||||
{
|
||||
"Space",
|
||||
" ",
|
||||
luma::keyboard_key_kind::SPACE,
|
||||
},
|
||||
{
|
||||
"Enter",
|
||||
"",
|
||||
luma::keyboard_key_kind::ENTER,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
auto luma::numeric_keyboard() -> luma::keyboard_layout {
|
||||
return {
|
||||
.rows = {
|
||||
{
|
||||
{
|
||||
"1",
|
||||
"1",
|
||||
},
|
||||
{
|
||||
"2",
|
||||
"2",
|
||||
},
|
||||
{
|
||||
"3",
|
||||
"3",
|
||||
},
|
||||
},
|
||||
{
|
||||
{
|
||||
"4",
|
||||
"4",
|
||||
},
|
||||
{
|
||||
"5",
|
||||
"5",
|
||||
},
|
||||
{
|
||||
"6",
|
||||
"6",
|
||||
},
|
||||
},
|
||||
{
|
||||
{
|
||||
"7",
|
||||
"7",
|
||||
},
|
||||
{
|
||||
"8",
|
||||
"8",
|
||||
},
|
||||
{
|
||||
"9",
|
||||
"9",
|
||||
},
|
||||
},
|
||||
{
|
||||
{
|
||||
"0",
|
||||
"0",
|
||||
},
|
||||
{
|
||||
".",
|
||||
".",
|
||||
},
|
||||
{
|
||||
"<-",
|
||||
"",
|
||||
luma::keyboard_key_kind::BACKSPACE,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user