@@ -16,10 +16,16 @@ add_library(luma
|
||||
src/detail/errors.cpp
|
||||
src/animation.cpp
|
||||
src/application.cpp
|
||||
src/detail/raylib_backend.cpp
|
||||
src/assets.cpp
|
||||
src/config.cpp
|
||||
src/control.cpp
|
||||
src/controls.cpp
|
||||
src/detail/control_geometry.cpp
|
||||
src/basic_controls.cpp
|
||||
src/text_controls.cpp
|
||||
src/layout_controls.cpp
|
||||
src/text_input.cpp
|
||||
src/overlays.cpp
|
||||
src/routing.cpp
|
||||
src/theme.cpp
|
||||
|
||||
@@ -4,6 +4,8 @@ Luma is a C++23 embedded touch UI library. It provides the reusable application
|
||||
|
||||
Luma uses Modulus for shared build tooling, Boost.JSON, and raylib integration. Product-specific screens and workflows belong in consuming applications.
|
||||
|
||||
Private implementations separate application lifecycle and dispatch from the raylib resource/drawing backend. Basic, text, and layout controls have separate implementations, with shared private geometry helpers. GPU resource creation and destruction remain on the UI thread, including deferred texture cleanup and retired controls.
|
||||
|
||||
## Build
|
||||
|
||||
Code style is managed by Modulus: run `./modulus.py format` or `./modulus.py format --check`. The generated Neovim integration uses the same pinned container formatter for C++ and Python, including existing format-on-save toggles. Configure first to prepare tooling; customize `tooling.editor.clang_format` and `tooling.format.exclude` in `modulus.json`, not generated editor files. See the shared Modulus README for the compact layout and no-alias policy.
|
||||
|
||||
+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