Cleanup
publish / publish (push) Failing after 9m5s

This commit is contained in:
khodowany
2026-10-08 02:13:15 -05:00
parent 5967db6bb8
commit 9a203321d1
12 changed files with 2135 additions and 1978 deletions
+11 -467
View File
@@ -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);
+157
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+168
View File
@@ -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,
};
}
}
+213
View File
@@ -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;
}
+423
View File
@@ -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,
};
}
}
+102
View File
@@ -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;
}
+392
View File
@@ -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{};
}
}
+185
View File
@@ -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;
}
}
+440
View File
@@ -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,
},
},
},
};
}