From 9a203321d190902211825720efd9951db5254a84 Mon Sep 17 00:00:00 2001 From: khodowany Date: Thu, 8 Oct 2026 02:13:15 -0500 Subject: [PATCH] Cleanup --- CMakeLists.txt | 6 + README.md | 2 + src/application.cpp | 478 +--------- src/basic_controls.cpp | 157 ++++ src/controls.cpp | 1547 +------------------------------ src/detail/control_geometry.cpp | 168 ++++ src/detail/control_types.hpp | 213 +++++ src/detail/raylib_backend.cpp | 423 +++++++++ src/detail/raylib_backend.hpp | 102 ++ src/layout_controls.cpp | 392 ++++++++ src/text_controls.cpp | 185 ++++ src/text_input.cpp | 440 +++++++++ 12 files changed, 2135 insertions(+), 1978 deletions(-) create mode 100644 src/basic_controls.cpp create mode 100644 src/detail/control_geometry.cpp create mode 100644 src/detail/control_types.hpp create mode 100644 src/detail/raylib_backend.cpp create mode 100644 src/detail/raylib_backend.hpp create mode 100644 src/layout_controls.cpp create mode 100644 src/text_controls.cpp create mode 100644 src/text_input.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 47e85a1..10c060e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/README.md b/README.md index ab70335..51c5562 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/src/application.cpp b/src/application.cpp index efb72e2..52d3531 100644 --- a/src/application.cpp +++ b/src/application.cpp @@ -1,4 +1,5 @@ #include +#include "detail/raylib_backend.hpp" #include #include @@ -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> records{}; - std::vector> pending_unloads{}; - std::unordered_map fonts{}; -}; - -class raylib_texture final : public luma::texture { -public: - raylib_texture(std::shared_ptr record, std::shared_ptr 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 record_{}; - std::shared_ptr state_{}; -}; - -class raylib_resources final : public luma::renderer_resources { -public: - explicit raylib_resources(std::shared_ptr state); - auto load_texture(std::string const &path) -> std::expected, modulus::error> override; - auto state() const -> std::shared_ptr; -private: - std::shared_ptr state_{}; -}; - -class raylib_draw_context final : public luma::draw_context { -public: - explicit raylib_draw_context(std::shared_ptr 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 state_{}; - std::vector clip_stack_{}; -}; - class scope_exit { public: explicit scope_exit(std::function 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 auto unavailable(std::string message) -> std::expected { return luma::detail::fail(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>{}; - { - 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(flush_pending_unloads(state)); - auto records = std::vector>{}; - { - 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(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::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(bounds.x), static_cast(bounds.y), static_cast(bounds.width), static_cast(bounds.height)); -} - -auto poll_touch(std::unordered_map &previous, std::int32_t max_points) -> luma::touch_event { - assert(IsWindowReady()); - assert(max_points > 0); - auto current = std::unordered_map{}; - auto points = std::vector{}; - 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{}(key.path); - seed ^= std::hash{}(key.size) + 0x9e3779b9U + (seed << 6U) + (seed >> 2U); - return seed; -} - scope_exit::scope_exit(std::function 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 record, std::shared_ptr 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 state) - : state_{std::move(state)} { - assert(state_ != nullptr); -} - -auto raylib_resources::load_texture(std::string const &path) -> std::expected, 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>(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>(luma::detail::error_not_found, "failed to load texture: " + path); - } - auto record = std::make_shared(); - record->texture = texture; - { - auto lock = std::scoped_lock{state_->mutex}; - state_->records.push_back(record); - } - return modulus::success(std::make_shared(record, state_)); -} - -auto raylib_resources::state() const -> std::shared_ptr { - return state_; -} - -raylib_draw_context::raylib_draw_context(std::shared_ptr 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::lround(center_x)), static_cast(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(index) / static_cast(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(&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 { core->running.store(true, std::memory_order_release); core->accepting_tasks.store(false, std::memory_order_release); } - auto resource_state = std::shared_ptr{}; + auto resource_state = std::shared_ptr{}; 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 { 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 { 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(); + resource_state = std::make_shared(); 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(resource_state); + core->resources = std::make_unique(resource_state); core->assets = std::make_unique(*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{}; 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 { 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 { } 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); diff --git a/src/basic_controls.cpp b/src/basic_controls.cpp new file mode 100644 index 0000000..80098ae --- /dev/null +++ b/src/basic_controls.cpp @@ -0,0 +1,157 @@ +#include "detail/control_types.hpp" + +#include +#include + + +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 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 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(info.width), + .height = static_cast(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(info.width), + .height = static_cast(info.height), + }; + context.draw_texture(*texture_, source, fit_rect(bounds(), info, fit_), theme.text); +} + +} diff --git a/src/controls.cpp b/src/controls.cpp index 554d806..7c7bfa4 100644 --- a/src/controls.cpp +++ b/src/controls.cpp @@ -1,1496 +1,21 @@ -#include +#include "detail/control_types.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include -#include -#include -#include -#include - - -namespace { - -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 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 on_click_; - std::optional pressed_id_; -}; - -class text_field_control final : public luma::control { -public: - explicit text_field_control(std::shared_ptr 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 invalidator) -> void override; - auto focusable() const -> std::bool_t override; -private: - std::shared_ptr input_; - std::shared_ptr layout_listener_lifetime_{}; - mutable std::optional cached_limits_{}; - mutable std::optional 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 on_text, std::function on_enter); - soft_keyboard_control(luma::keyboard_layout layout, std::shared_ptr input, std::function 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 on_text_; - std::function on_enter_; - std::shared_ptr input_{}; -}; - -class panel_control final : public luma::control { -public: - explicit panel_control(std::vector> 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 invalidator) -> void override; -private: - std::vector> 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 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 texture_; - luma::image_fit fit_; -}; - -class linear_control final : public luma::control { -public: - linear_control(luma::direction direction, std::vector> 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 invalidator) -> void override; -private: - luma::direction direction_; - std::vector> children_; - std::float32_t gap_; - luma::alignment align_; - std::vector measured_; -}; - -class stack_control final : public luma::control { -public: - explicit stack_control(std::vector> 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 invalidator) -> void override; -private: - std::vector> children_; -}; - -class scroll_view_control final : public luma::control { -public: - scroll_view_control(std::unique_ptr 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 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 child_; - std::bool_t vertical_; - std::bool_t horizontal_; - luma::size content_size_{}; - std::optional cached_child_limits_{}; - std::optional cached_theme_{}; - luma::size cached_content_size_{}; - std::bool_t content_measure_valid_{false}; - luma::point offset_{}; - luma::point velocity_{}; - std::unordered_map touches_{}; - std::optional scroll_id_{}; - std::bool_t dragging_{false}; -}; - -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; -} - -template -auto invalid(std::string message) -> std::expected { - return luma::detail::fail(luma::detail::error_invalid_argument, std::move(message)); -} - -auto throw_error(modulus::error const &error) -> void { - throw std::runtime_error(error.describe()); -} - -auto throw_if_failed(std::expected 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> &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 { - if (layout.rows.empty()) { - return luma::detail::fail(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(luma::detail::error_invalid_argument, "keyboard key label must not be empty"); - } - } - } - if (!has_key) { - return luma::detail::fail(luma::detail::error_invalid_argument, "keyboard layout must contain at least one key"); - } - return {}; -} - -auto validate_children(std::vector> const &children) -> std::expected { - for (auto const &child : children) { - if (child == nullptr) { - return luma::detail::fail(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(info.width); - auto source_height = static_cast(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, - }; -} - -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 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; -} - -text_field_control::text_field_control(std::shared_ptr 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 invalidator) -> void { - luma::control::bind_layout_invalidator(invalidator); - if (!invalidator) { - layout_listener_lifetime_.reset(); - return; - } - layout_listener_lifetime_ = std::make_shared(); - 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 on_text, std::function 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 input, std::function 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(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(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(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(column_index), - .y = bounds().y + key_height * static_cast(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((point->position.y - bounds().y) / (bounds().height / static_cast(row_count)))); - auto const &row = layout_.rows[row_index]; - if (row.empty()) { - return true; - } - auto column_index = std::min(row.size() - 1, static_cast((point->position.x - bounds().x) / (bounds().width / static_cast(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; -} - -panel_control::panel_control(std::vector> 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 invalidator) -> void { - luma::control::bind_layout_invalidator(invalidator); - for (auto &child : children_) { - assert(child != nullptr); - child->bind_layout_invalidator(invalidator); - } -} - -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 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(info.width), - .height = static_cast(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(info.width), - .height = static_cast(info.height), - }; - context.draw_texture(*texture_, source, fit_rect(bounds(), info, fit_), theme.text); -} - -linear_control::linear_control(luma::direction direction, std::vector> 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 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> 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 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 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::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(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(child_->handle_touch(make_event(event, cancel))); - } else { - static_cast(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 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{}; -} - -} - -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 { - if (options.max_length == 0) { - return luma::detail::fail(luma::detail::error_invalid_argument, "text field max length must be positive"); - } - if (options.text.size() > options.max_length) { - return luma::detail::fail(luma::detail::error_invalid_argument, "text field initial text exceeds max length"); - } - if (!accepts_text(options.mode, "", options.text)) { - return luma::detail::fail(luma::detail::error_invalid_argument, "text field initial text does not match input mode"); - } - return {}; -} - -auto callback_failure(std::string message) -> std::expected { - return luma::detail::fail(luma::detail::error_internal, std::move(message)); -} - -auto notify_text_changed(std::function const &callback, std::string value) -> std::expected { - 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> const &listeners) -> void { - for (auto const &listener : listeners) { - if (!listener) { - continue; - } - try { - listener(); - } catch (...) { - } - } -} - -auto collect_layout_listeners(std::vector &listeners) -> std::vector> { - auto callbacks = std::vector>{}; - 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 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 lifetime, std::function 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 { - if (value == "\b") { - return backspace(); - } - auto callback_text = std::string{}; - auto layout_listeners = std::vector>{}; - 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(luma::detail::error_invalid_argument, "text field max length exceeded"); - } - if (!accepts_text(options_.mode, options_.text, value)) { - return luma::detail::fail(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 { - auto callback_text = std::string{}; - auto layout_listeners = std::vector>{}; - 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 { - auto callback_text = std::string{}; - auto layout_listeners = std::vector>{}; - { - auto lock = std::scoped_lock{mutex_}; - if (value.size() > options_.max_length) { - return luma::detail::fail(luma::detail::error_invalid_argument, "text field max length exceeded"); - } - if (!accepts_text(options_.mode, "", value)) { - return luma::detail::fail(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 on_change) -> std::expected, modulus::error> { - if (!on_change) { - return luma::detail::fail>(luma::detail::error_invalid_argument, "text field change callback must not be empty"); - } - if (auto status = validate_text_options(options); !status) { - return modulus::fail>(status.error()); - } - return modulus::success(std::make_shared(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, - }, - }, - }, - }; -} auto luma::label(std::string text) -> std::expected, modulus::error> { if (text.empty()) { - return invalid>("label text must not be empty"); + return luma::detail::invalid>("label text must not be empty"); } - return modulus::success(std::unique_ptr{std::make_unique(std::move(text))}); + return modulus::success(std::unique_ptr{std::make_unique(std::move(text))}); } auto luma::button(std::string text, std::function on_click) -> std::expected, modulus::error> { if (text.empty()) { - return invalid>("button text must not be empty"); + return luma::detail::invalid>("button text must not be empty"); } if (!on_click) { - return invalid>("button click callback must not be empty"); + return luma::detail::invalid>("button click callback must not be empty"); } - return modulus::success(std::unique_ptr{std::make_unique(std::move(text), std::move(on_click))}); + return modulus::success(std::unique_ptr{std::make_unique(std::move(text), std::move(on_click))}); } auto luma::text_field(luma::text_field_options options, std::function on_change) -> std::expected, modulus::error> { @@ -1503,29 +28,29 @@ auto luma::text_field(luma::text_field_options options, std::function input) -> std::expected, modulus::error> { if (input == nullptr) { - return invalid>("text input state must not be null"); + return luma::detail::invalid>("text input state must not be null"); } - return modulus::success(std::unique_ptr{std::make_unique(std::move(input))}); + return modulus::success(std::unique_ptr{std::make_unique(std::move(input))}); } auto luma::soft_keyboard(luma::keyboard_layout layout, std::shared_ptr input, std::function on_enter) -> std::expected, modulus::error> { - if (auto status = valid_keyboard_layout(layout); !status) { + if (auto status = luma::detail::valid_keyboard_layout(layout); !status) { return modulus::fail>(status.error()); } if (input == nullptr) { - return invalid>("text input state must not be null"); + return luma::detail::invalid>("text input state must not be null"); } - return modulus::success(std::unique_ptr{std::make_unique(std::move(layout), std::move(input), std::move(on_enter))}); + return modulus::success(std::unique_ptr{std::make_unique(std::move(layout), std::move(input), std::move(on_enter))}); } auto luma::soft_keyboard(luma::keyboard_layout layout, std::function on_text, std::function on_enter) -> std::expected, modulus::error> { - if (auto status = valid_keyboard_layout(layout); !status) { + if (auto status = luma::detail::valid_keyboard_layout(layout); !status) { return modulus::fail>(status.error()); } if (!on_text) { - return invalid>("keyboard text callback must not be empty"); + return luma::detail::invalid>("keyboard text callback must not be empty"); } - return modulus::success(std::unique_ptr{std::make_unique(std::move(layout), std::move(on_text), std::move(on_enter))}); + return modulus::success(std::unique_ptr{std::make_unique(std::move(layout), std::move(on_text), std::move(on_enter))}); } auto luma::panel(std::expected>, modulus::error> children) -> std::expected, modulus::error> { @@ -1536,35 +61,35 @@ auto luma::panel(std::expected>, modu } auto luma::panel(std::vector> children) -> std::expected, modulus::error> { - if (auto status = validate_children(children); !status) { + if (auto status = luma::detail::validate_children(children); !status) { return modulus::fail>(status.error()); } - return modulus::success(std::unique_ptr{std::make_unique(std::move(children))}); + return modulus::success(std::unique_ptr{std::make_unique(std::move(children))}); } auto luma::spacer(luma::size value) -> std::expected, modulus::error> { - if (!finite_size(value)) { - return invalid>("spacer size must be finite and non-negative"); + if (!luma::detail::finite_size(value)) { + return luma::detail::invalid>("spacer size must be finite and non-negative"); } - return modulus::success(std::unique_ptr{std::make_unique(value)}); + return modulus::success(std::unique_ptr{std::make_unique(value)}); } auto luma::progress_bar(std::float32_t value) -> std::expected, modulus::error> { if (!std::isfinite(value)) { - return invalid>("progress value must be finite"); + return luma::detail::invalid>("progress value must be finite"); } - return modulus::success(std::unique_ptr{std::make_unique(value)}); + return modulus::success(std::unique_ptr{std::make_unique(value)}); } auto luma::image(std::shared_ptr texture, luma::image_fit fit) -> std::expected, modulus::error> { if (texture == nullptr) { - return invalid>("image texture must not be null"); + return luma::detail::invalid>("image texture must not be null"); } auto info = texture->info(); if (info.width <= 0 || info.height <= 0) { - return invalid>("image texture dimensions must be positive"); + return luma::detail::invalid>("image texture dimensions must be positive"); } - return modulus::success(std::unique_ptr{std::make_unique(std::move(texture), fit)}); + return modulus::success(std::unique_ptr{std::make_unique(std::move(texture), fit)}); } auto luma::row(std::expected>, modulus::error> children, std::float32_t gap, luma::alignment align) -> std::expected, modulus::error> { @@ -1575,13 +100,13 @@ auto luma::row(std::expected>, modulu } auto luma::row(std::vector> children, std::float32_t gap, luma::alignment align) -> std::expected, modulus::error> { - if (auto status = validate_children(children); !status) { + if (auto status = luma::detail::validate_children(children); !status) { return modulus::fail>(status.error()); } if (!std::isfinite(gap) || gap < 0.0F) { - return invalid>("row gap must be finite and non-negative"); + return luma::detail::invalid>("row gap must be finite and non-negative"); } - return modulus::success(std::unique_ptr{std::make_unique(luma::direction::ROW, std::move(children), gap, align)}); + return modulus::success(std::unique_ptr{std::make_unique(luma::direction::ROW, std::move(children), gap, align)}); } auto luma::column(std::expected>, modulus::error> children, std::float32_t gap, luma::alignment align) -> std::expected, modulus::error> { @@ -1592,13 +117,13 @@ auto luma::column(std::expected>, mod } auto luma::column(std::vector> children, std::float32_t gap, luma::alignment align) -> std::expected, modulus::error> { - if (auto status = validate_children(children); !status) { + if (auto status = luma::detail::validate_children(children); !status) { return modulus::fail>(status.error()); } if (!std::isfinite(gap) || gap < 0.0F) { - return invalid>("column gap must be finite and non-negative"); + return luma::detail::invalid>("column gap must be finite and non-negative"); } - return modulus::success(std::unique_ptr{std::make_unique(luma::direction::COLUMN, std::move(children), gap, align)}); + return modulus::success(std::unique_ptr{std::make_unique(luma::direction::COLUMN, std::move(children), gap, align)}); } auto luma::stack(std::expected>, modulus::error> children) -> std::expected, modulus::error> { @@ -1609,10 +134,10 @@ auto luma::stack(std::expected>, modu } auto luma::stack(std::vector> children) -> std::expected, modulus::error> { - if (auto status = validate_children(children); !status) { + if (auto status = luma::detail::validate_children(children); !status) { return modulus::fail>(status.error()); } - return modulus::success(std::unique_ptr{std::make_unique(std::move(children))}); + return modulus::success(std::unique_ptr{std::make_unique(std::move(children))}); } auto luma::scroll_view(std::expected, modulus::error> child, std::bool_t vertical, std::bool_t horizontal) -> std::expected, modulus::error> { @@ -1624,12 +149,12 @@ auto luma::scroll_view(std::expected, modulus::er auto luma::scroll_view(std::unique_ptr child, std::bool_t vertical, std::bool_t horizontal) -> std::expected, modulus::error> { if (child == nullptr) { - return invalid>("scroll view child must not be null"); + return luma::detail::invalid>("scroll view child must not be null"); } if (!vertical && !horizontal) { - return invalid>("scroll view must enable at least one axis"); + return luma::detail::invalid>("scroll view must enable at least one axis"); } - return modulus::success(std::unique_ptr{std::make_unique(std::move(child), vertical, horizontal)}); + return modulus::success(std::unique_ptr{std::make_unique(std::move(child), vertical, horizontal)}); } auto luma::list(std::expected>, modulus::error> children, std::float32_t gap) -> std::expected, modulus::error> { @@ -1641,7 +166,7 @@ auto luma::list(std::expected>, modul auto luma::list(std::vector> children, std::float32_t gap) -> std::expected, modulus::error> { if (!std::isfinite(gap) || gap < 0.0F) { - return invalid>("list gap must be finite and non-negative"); + return luma::detail::invalid>("list gap must be finite and non-negative"); } return luma::column(std::move(children), gap, luma::alignment::STRETCH); } diff --git a/src/detail/control_geometry.cpp b/src/detail/control_geometry.cpp new file mode 100644 index 0000000..e739717 --- /dev/null +++ b/src/detail/control_geometry.cpp @@ -0,0 +1,168 @@ +#include "control_types.hpp" + +#include +#include +#include + + +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 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> &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 { + if (layout.rows.empty()) { + return luma::detail::fail(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(luma::detail::error_invalid_argument, "keyboard key label must not be empty"); + } + } + } + if (!has_key) { + return luma::detail::fail(luma::detail::error_invalid_argument, "keyboard layout must contain at least one key"); + } + return {}; +} + +auto validate_children(std::vector> const &children) -> std::expected { + for (auto const &child : children) { + if (child == nullptr) { + return luma::detail::fail(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(info.width); + auto source_height = static_cast(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, + }; +} + +} diff --git a/src/detail/control_types.hpp b/src/detail/control_types.hpp new file mode 100644 index 0000000..76cab78 --- /dev/null +++ b/src/detail/control_types.hpp @@ -0,0 +1,213 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +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 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 on_click_; + std::optional pressed_id_; +}; + +class text_field_control final : public luma::control { +public: + explicit text_field_control(std::shared_ptr 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 invalidator) -> void override; + auto focusable() const -> std::bool_t override; +private: + std::shared_ptr input_; + std::shared_ptr layout_listener_lifetime_{}; + mutable std::optional cached_limits_{}; + mutable std::optional 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 on_text, std::function on_enter); + soft_keyboard_control(luma::keyboard_layout layout, std::shared_ptr input, std::function 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 on_text_; + std::function on_enter_; + std::shared_ptr input_{}; +}; + +class panel_control final : public luma::control { +public: + explicit panel_control(std::vector> 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 invalidator) -> void override; +private: + std::vector> 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 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 texture_; + luma::image_fit fit_; +}; + +class linear_control final : public luma::control { +public: + linear_control(luma::direction direction, std::vector> 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 invalidator) -> void override; +private: + luma::direction direction_; + std::vector> children_; + std::float32_t gap_; + luma::alignment align_; + std::vector measured_; +}; + +class stack_control final : public luma::control { +public: + explicit stack_control(std::vector> 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 invalidator) -> void override; +private: + std::vector> children_; +}; + +class scroll_view_control final : public luma::control { +public: + scroll_view_control(std::unique_ptr 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 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 child_; + std::bool_t vertical_; + std::bool_t horizontal_; + luma::size content_size_{}; + std::optional cached_child_limits_{}; + std::optional cached_theme_{}; + luma::size cached_content_size_{}; + std::bool_t content_measure_valid_{false}; + luma::point offset_{}; + luma::point velocity_{}; + std::unordered_map touches_{}; + std::optional 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 +auto invalid(std::string message) -> std::expected { + return luma::detail::fail(luma::detail::error_invalid_argument, std::move(message)); +} + +auto throw_error(modulus::error const &error) -> void; +auto throw_if_failed(std::expected const &status) -> void; +auto make_event(luma::touch_event const &event, luma::touch_point point) -> luma::touch_event; +auto dispatch_to_children(std::vector> &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; +auto validate_children(std::vector> const &children) -> std::expected; +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; + +} diff --git a/src/detail/raylib_backend.cpp b/src/detail/raylib_backend.cpp new file mode 100644 index 0000000..61cdaa2 --- /dev/null +++ b/src/detail/raylib_backend.cpp @@ -0,0 +1,423 @@ +#include "raylib_backend.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace luma::detail { + +namespace { + +template +auto unavailable(std::string message) -> std::expected { + return luma::detail::fail(luma::detail::error_unavailable, std::move(message)); +} + +template +auto internal(std::string message) -> std::expected { + return luma::detail::fail(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>{}; + { + 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(flush_pending_unloads(state)); + auto records = std::vector>{}; + { + 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(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::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(bounds.x), static_cast(bounds.y), static_cast(bounds.width), static_cast(bounds.height)); +} + +auto poll_touch(std::unordered_map &previous, std::int32_t max_points) -> luma::touch_event { + assert(IsWindowReady()); + assert(max_points > 0); + auto current = std::unordered_map{}; + auto points = std::vector{}; + 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{}(key.path); + seed ^= std::hash{}(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 record, std::shared_ptr 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 state) + : state_{std::move(state)} { + assert(state_ != nullptr); +} + +auto raylib_resources::load_texture(std::string const &path) -> std::expected, 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>(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>(luma::detail::error_not_found, "failed to load texture: " + path); + } + auto record = std::make_shared(); + record->texture = texture; + { + auto lock = std::scoped_lock{state_->mutex}; + state_->records.push_back(record); + } + return modulus::success(std::make_shared(record, state_)); +} + +auto raylib_resources::state() const -> std::shared_ptr { + return state_; +} + +raylib_draw_context::raylib_draw_context(std::shared_ptr 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::lround(center_x)), static_cast(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(index) / static_cast(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(&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, + }; +} + +} diff --git a/src/detail/raylib_backend.hpp b/src/detail/raylib_backend.hpp new file mode 100644 index 0000000..a2098ed --- /dev/null +++ b/src/detail/raylib_backend.hpp @@ -0,0 +1,102 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +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> records{}; + std::vector> pending_unloads{}; + std::unordered_map fonts{}; +}; + +class raylib_texture final : public luma::texture { +public: + raylib_texture(std::shared_ptr record, std::shared_ptr 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 record_{}; + std::shared_ptr state_{}; +}; + +class raylib_resources final : public luma::renderer_resources { +public: + explicit raylib_resources(std::shared_ptr state); + auto load_texture(std::string const &path) -> std::expected, modulus::error> override; + auto state() const -> std::shared_ptr; +private: + std::shared_ptr state_{}; +}; + +class raylib_draw_context final : public luma::draw_context { +public: + explicit raylib_draw_context(std::shared_ptr 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 state_{}; + std::vector 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 &previous, std::int32_t max_points) -> luma::touch_event; + +} diff --git a/src/layout_controls.cpp b/src/layout_controls.cpp new file mode 100644 index 0000000..eb5740b --- /dev/null +++ b/src/layout_controls.cpp @@ -0,0 +1,392 @@ +#include "detail/control_types.hpp" + +#include +#include +#include + + +namespace luma::detail { + +panel_control::panel_control(std::vector> 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 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> 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 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> 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 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 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::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(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(child_->handle_touch(make_event(event, cancel))); + } else { + static_cast(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 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{}; +} + +} diff --git a/src/text_controls.cpp b/src/text_controls.cpp new file mode 100644 index 0000000..7a2c99f --- /dev/null +++ b/src/text_controls.cpp @@ -0,0 +1,185 @@ +#include "detail/control_types.hpp" + +#include + + +namespace luma::detail { + +text_field_control::text_field_control(std::shared_ptr 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 invalidator) -> void { + luma::control::bind_layout_invalidator(invalidator); + if (!invalidator) { + layout_listener_lifetime_.reset(); + return; + } + layout_listener_lifetime_ = std::make_shared(); + 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 on_text, std::function 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 input, std::function 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(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(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(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(column_index), + .y = bounds().y + key_height * static_cast(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((point->position.y - bounds().y) / (bounds().height / static_cast(row_count)))); + auto const &row = layout_.rows[row_index]; + if (row.empty()) { + return true; + } + auto column_index = std::min(row.size() - 1, static_cast((point->position.x - bounds().x) / (bounds().width / static_cast(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; +} + +} diff --git a/src/text_input.cpp b/src/text_input.cpp new file mode 100644 index 0000000..10a5a8a --- /dev/null +++ b/src/text_input.cpp @@ -0,0 +1,440 @@ +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + + +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 { + if (options.max_length == 0) { + return luma::detail::fail(luma::detail::error_invalid_argument, "text field max length must be positive"); + } + if (options.text.size() > options.max_length) { + return luma::detail::fail(luma::detail::error_invalid_argument, "text field initial text exceeds max length"); + } + if (!accepts_text(options.mode, "", options.text)) { + return luma::detail::fail(luma::detail::error_invalid_argument, "text field initial text does not match input mode"); + } + return {}; +} + +auto callback_failure(std::string message) -> std::expected { + return luma::detail::fail(luma::detail::error_internal, std::move(message)); +} + +auto notify_text_changed(std::function const &callback, std::string value) -> std::expected { + 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> const &listeners) -> void { + for (auto const &listener : listeners) { + if (!listener) { + continue; + } + try { + listener(); + } catch (...) { + } + } +} + +auto collect_layout_listeners(std::vector &listeners) -> std::vector> { + auto callbacks = std::vector>{}; + 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 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 lifetime, std::function 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 { + if (value == "\b") { + return backspace(); + } + auto callback_text = std::string{}; + auto layout_listeners = std::vector>{}; + 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(luma::detail::error_invalid_argument, "text field max length exceeded"); + } + if (!accepts_text(options_.mode, options_.text, value)) { + return luma::detail::fail(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 { + auto callback_text = std::string{}; + auto layout_listeners = std::vector>{}; + 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 { + auto callback_text = std::string{}; + auto layout_listeners = std::vector>{}; + { + auto lock = std::scoped_lock{mutex_}; + if (value.size() > options_.max_length) { + return luma::detail::fail(luma::detail::error_invalid_argument, "text field max length exceeded"); + } + if (!accepts_text(options_.mode, "", value)) { + return luma::detail::fail(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 on_change) -> std::expected, modulus::error> { + if (!on_change) { + return luma::detail::fail>(luma::detail::error_invalid_argument, "text field change callback must not be empty"); + } + if (auto status = validate_text_options(options); !status) { + return modulus::fail>(status.error()); + } + return modulus::success(std::make_shared(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, + }, + }, + }, + }; +}