Initial public khodo snapshot
publish / publish (push) Successful in 27m4s

This commit is contained in:
khodowany
2026-09-04 19:30:11 -05:00
commit 51339606a2
38 changed files with 8989 additions and 0 deletions
+105
View File
@@ -0,0 +1,105 @@
name: publish
on:
push:
branches:
- master
pull_request:
workflow_dispatch:
permissions:
contents: read
packages: write
actions: write
env:
CONAN_HOME: build/build_common/conan_cache
MODULUS_DIRECT: "1"
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install build dependencies
run: |
if [ -r /etc/os-release ]; then
. /etc/os-release
fi
if [ "${ID:-}" = "ubuntu" ] && [ -n "${VERSION_CODENAME:-}" ]; then
if [ -f /etc/apt/sources.list.d/ubuntu.sources ]; then
sed -i '/^Types: deb$/a Architectures: amd64' /etc/apt/sources.list.d/ubuntu.sources
printf '%s\n' \
'Types: deb' \
'URIs: http://ports.ubuntu.com/ubuntu-ports/' \
"Suites: ${VERSION_CODENAME} ${VERSION_CODENAME}-updates ${VERSION_CODENAME}-backports" \
'Components: main universe restricted multiverse' \
'Architectures: arm64' \
'Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg' \
'' \
'Types: deb' \
'URIs: http://ports.ubuntu.com/ubuntu-ports/' \
"Suites: ${VERSION_CODENAME}-security" \
'Components: main universe restricted multiverse' \
'Architectures: arm64' \
'Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg' \
> /etc/apt/sources.list.d/ubuntu-arm64.sources
elif [ -f /etc/apt/sources.list ]; then
sed -i 's|^deb http://archive.ubuntu.com/ubuntu|deb [arch=amd64] http://archive.ubuntu.com/ubuntu|' /etc/apt/sources.list
sed -i 's|^deb http://security.ubuntu.com/ubuntu|deb [arch=amd64] http://security.ubuntu.com/ubuntu|' /etc/apt/sources.list
printf '%s\n' \
"deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports ${VERSION_CODENAME} main universe restricted multiverse" \
"deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports ${VERSION_CODENAME}-updates main universe restricted multiverse" \
"deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports ${VERSION_CODENAME}-backports main universe restricted multiverse" \
"deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports ${VERSION_CODENAME}-security main universe restricted multiverse" \
> /etc/apt/sources.list.d/ubuntu-arm64.list
fi
fi
dpkg --add-architecture arm64
apt-get update
apt-get install -y --no-install-recommends \
build-essential \
ca-certificates \
cmake \
g++-12 \
g++-aarch64-linux-gnu \
gcc-12 \
gcc-aarch64-linux-gnu \
git \
libdrm-dev \
libdrm-dev:arm64 \
libegl-dev \
libegl-dev:arm64 \
libgbm-dev \
libgbm-dev:arm64 \
libgles-dev \
libgles-dev:arm64 \
libglvnd-dev \
libglvnd-dev:arm64 \
ninja-build \
pkg-config \
python3 \
python3-venv
python3 -m venv build/build_common/ci_python
build/build_common/ci_python/bin/python -m pip install --no-cache-dir conan==2.26.2
echo "$PWD/build/build_common/ci_python/bin" >> "$GITHUB_PATH"
- name: Restore Conan cache
uses: actions/cache@v4
with:
path: build/build_common/conan_cache
key: conan-${{ runner.os }}-${{ hashFiles('conanfile.py', 'modulus.json') }}
restore-keys: conan-${{ runner.os }}-
- name: Configure native release
run: python3 modulus.py configure --target x86_64_gcc12_release
- name: Build native release
run: python3 modulus.py build --target x86_64_gcc12_release
- name: Test native release
run: python3 modulus.py test --target x86_64_gcc12_release
- name: Create and upload packages
if: ${{ gitea.event_name == 'workflow_dispatch' || (gitea.event_name == 'push' && gitea.ref == 'refs/heads/master') }}
env:
CONAN_USER: ${{ secrets.CONAN_USER }}
CONAN_TOKEN: ${{ secrets.CONAN_TOKEN }}
LUMA_CONAN_UPLOAD: "1"
MODULUS_ALLOW_PACKAGE_REPLACE: "1"
run: python3 modulus.py package --all --replace
+20
View File
@@ -0,0 +1,20 @@
build/
.clangd
.clang-format
pyrightconfig.json
**/__pycache__/
*.pyc
*.pyo
*.pyd
*.log
package/CMakeCache.txt
package/CMakeFiles/
package/metadata/
package/.ninja_*
package/*.json
package/*.cmake
package/*.ninja
package/*.sh
package/*-smoke
+77
View File
@@ -0,0 +1,77 @@
cmake_minimum_required(VERSION 3.25)
project(luma VERSION 0.1.0 LANGUAGES CXX)
include(GNUInstallDirs)
include(CMakePackageConfigHelpers)
include(CTest)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
find_package(modulus CONFIG REQUIRED)
add_library(luma
src/detail/errors.cpp
src/animation.cpp
src/application.cpp
src/assets.cpp
src/config.cpp
src/control.cpp
src/controls.cpp
src/overlays.cpp
src/routing.cpp
src/theme.cpp
src/window.cpp
)
target_include_directories(luma PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
target_compile_features(luma PUBLIC cxx_std_23)
target_compile_definitions(luma PRIVATE
LUMA_RAYLIB_CUSTOM_FRAME_CONTROL=1
)
target_link_libraries(luma PUBLIC
modulus::modulus
modulus::boost_json
modulus::raylib
)
add_library(luma::luma ALIAS luma)
if(BUILD_TESTING)
add_subdirectory(tests)
endif()
install(TARGETS luma
EXPORT luma-targets
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(EXPORT luma-targets
NAMESPACE luma::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/luma
)
configure_package_config_file(
${CMAKE_CURRENT_SOURCE_DIR}/cmake/luma_config.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/luma-config.cmake
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/luma
)
write_basic_package_version_file(
${CMAKE_CURRENT_BINARY_DIR}/luma-config-version.cmake
VERSION ${PROJECT_VERSION}
COMPATIBILITY SameMajorVersion
)
install(FILES
${CMAKE_CURRENT_BINARY_DIR}/luma-config.cmake
${CMAKE_CURRENT_BINARY_DIR}/luma-config-version.cmake
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/luma
)
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Khodo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+43
View File
@@ -0,0 +1,43 @@
# Luma
Luma is a C++23 embedded touch UI library. It provides the reusable application shell, view routing, layout primitives, touch input handling, focus state, themes, text rendering conventions, frame timing, and controls used by Product Vamp.
Luma uses Modulus for shared build tooling, Boost.JSON, and raylib integration. Product-specific screens and workflows belong in consuming applications.
## Build
Configure, build, and test the default target:
```sh
python3 modulus.py configure
python3 modulus.py build
python3 modulus.py test
```
Release verification:
```sh
python3 modulus.py configure --target x86_64_gcc12_release
python3 modulus.py build --target x86_64_gcc12_release
python3 modulus.py test --target x86_64_gcc12_release
```
Create packages:
```sh
python3 modulus.py package --all
```
Configure/build/test can read public packages anonymously from the khodo Conan registry. Publishing requires credentials only on the upload step:
```sh
CONAN_USER=<user> \
CONAN_TOKEN=<token> \
LUMA_CONAN_UPLOAD=1 \
MODULUS_ALLOW_PACKAGE_REPLACE=1 \
python3 modulus.py package --all --replace
```
## License
Luma is released under the MIT License.
+6
View File
@@ -0,0 +1,6 @@
@PACKAGE_INIT@
include(CMakeFindDependencyMacro)
find_dependency(modulus CONFIG REQUIRED)
include("${CMAKE_CURRENT_LIST_DIR}/luma-targets.cmake")
+104
View File
@@ -0,0 +1,104 @@
import conan
import conan.tools.build
import conan.tools.cmake
class luma_conan(conan.ConanFile):
name = "luma"
version = "latest"
description = "C++23 embedded touch UI library"
license = "MIT"
package_type = "library"
settings = "os", "arch", "compiler", "build_type"
options = {
"shared": [
True,
False,
],
"fPIC": [
True,
False,
],
"with_tests": [
True,
False,
],
}
default_options = {
"shared": False,
"fPIC": True,
"with_tests": False,
}
exports = "modulus.json"
exports_sources = (
"CMakeLists.txt",
"cmake/*.in",
"include/*.hpp",
"include/luma/*.hpp",
"include/luma/detail/*.hpp",
"src/*.cpp",
"tests/CMakeLists.txt",
"tests/*.cpp",
"tests/*.hpp",
)
def config_options(self):
if self.settings.os == "Windows":
del self.options.fPIC
def configure(self):
if self.options.shared:
del self.options.fPIC
def requirements(self):
self.requires(
"modulus/latest",
transitive_headers=True,
transitive_libs=True,
)
def generate(self):
deps = conan.tools.cmake.CMakeDeps(self)
deps.generate()
toolchain = conan.tools.cmake.CMakeToolchain(self)
if hasattr(toolchain, "user_presets_path"):
toolchain.user_presets_path = False
toolchain.variables["BUILD_SHARED_LIBS"] = bool(self.options.shared)
toolchain.variables["BUILD_TESTING"] = bool(self.options.with_tests)
toolchain.generate()
def build(self):
cmake = conan.tools.cmake.CMake(self)
cmake.configure()
cmake.build()
if self.options.with_tests and conan.tools.build.can_run(self):
cmake.test(target="check")
def package_id(self):
self.info.options.rm_safe("with_tests")
for requirement in (
"openssl",
"sqlite3",
"miniaudio",
):
if requirement in self.info.requires.pkg_names:
self.info.requires.remove(requirement)
def package(self):
cmake = conan.tools.cmake.CMake(self)
cmake.install()
def package_info(self):
self.cpp_info.set_property(
"cmake_file_name",
"luma",
)
self.cpp_info.set_property(
"cmake_target_name",
"luma::luma",
)
self.cpp_info.libs = ["luma"]
self.cpp_info.requires = [
"modulus::modulus",
"modulus::boost_json",
"modulus::raylib",
]
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include <luma/application.hpp>
#include <luma/animation.hpp>
#include <luma/assets.hpp>
#include <luma/config.hpp>
#include <luma/control.hpp>
#include <luma/controls.hpp>
#include <luma/geometry.hpp>
#include <luma/input.hpp>
#include <luma/overlays.hpp>
#include <luma/render.hpp>
#include <luma/routing.hpp>
#include <luma/theme.hpp>
#include <luma/window.hpp>
+54
View File
@@ -0,0 +1,54 @@
#pragma once
#include <chrono>
#include <expected>
#include <functional>
#include <vector>
#include <modulus.hpp>
namespace luma {
enum class easing {
LINEAR,
EASE_OUT,
EASE_IN_OUT,
};
struct animation {
std::float32_t from{};
std::float32_t to{};
std::chrono::milliseconds duration{200};
luma::easing easing{luma::easing::EASE_OUT};
std::function<void(std::float32_t)> apply{};
std::function<void()> finished{};
};
class animation_graph {
public:
auto add(luma::animation animation) -> std::expected<void, modulus::error>;
auto tick(std::chrono::steady_clock::duration delta) -> std::expected<void, modulus::error>;
auto empty() const -> std::bool_t;
auto clear() -> void;
private:
struct running_animation {
luma::animation animation{};
std::chrono::steady_clock::duration elapsed{};
};
enum class pending_kind {
ADD,
CLEAR,
};
struct pending_operation {
luma::animation_graph::pending_kind kind{luma::animation_graph::pending_kind::CLEAR};
luma::animation_graph::running_animation animation{};
};
std::vector<luma::animation_graph::running_animation> animations_{};
std::vector<luma::animation_graph::pending_operation> pending_{};
std::bool_t ticking_{false};
std::bool_t clear_requested_{false};
};
auto interpolate(std::float32_t from, std::float32_t to, std::float32_t progress, luma::easing easing) -> std::expected<std::float32_t, modulus::error>;
}
+81
View File
@@ -0,0 +1,81 @@
#pragma once
#include <chrono>
#include <cstdint>
#include <expected>
#include <functional>
#include <memory>
#include <mutex>
#include <string>
#include <type_traits>
#include <utility>
#include <modulus.hpp>
#include <luma/animation.hpp>
#include <luma/config.hpp>
#include <luma/control.hpp>
#include <luma/geometry.hpp>
#include <luma/overlays.hpp>
#include <luma/render.hpp>
#include <luma/window.hpp>
namespace luma {
namespace detail {
struct application_core;
}
struct frame_stats {
std::uint64_t frame{};
std::chrono::steady_clock::duration delta{};
};
class application {
public:
explicit application(luma::application_config config = {});
application(luma::application const&) = delete;
application(luma::application&&) noexcept;
~application();
auto operator=(luma::application const&) -> luma::application& = delete;
auto operator=(luma::application&&) noexcept -> luma::application&;
auto dispatch(std::function<std::expected<void, modulus::error>()> task) -> std::expected<void, modulus::error>;
auto register_route(std::string name, std::function<std::expected<std::unique_ptr<luma::control>, modulus::error>()> factory) -> std::expected<void, modulus::error>;
auto replace_route(std::string name) -> std::expected<void, modulus::error>;
auto push_route(std::string name) -> std::expected<void, modulus::error>;
auto pop_route() -> std::expected<void, modulus::error>;
auto current_route() const -> std::expected<std::string, modulus::error>;
auto add_animation(luma::animation animation) -> std::expected<void, modulus::error>;
auto clear_animations() -> std::expected<void, modulus::error>;
auto animations_empty() const -> std::expected<std::bool_t, modulus::error>;
auto open_window(luma::window_options options, std::unique_ptr<luma::control> content) -> std::expected<std::uint64_t, modulus::error>;
auto close_window(std::uint64_t id) -> std::expected<void, modulus::error>;
auto bring_window_to_front(std::uint64_t id) -> std::expected<void, modulus::error>;
auto set_window_bounds(std::uint64_t id, luma::rect bounds) -> std::expected<void, modulus::error>;
auto set_window_visible(std::uint64_t id, std::bool_t visible) -> std::expected<void, modulus::error>;
auto load_texture(std::string path) -> std::expected<std::shared_ptr<luma::texture>, modulus::error>;
auto request_layout() -> std::expected<void, modulus::error>;
auto set_root(std::expected<std::unique_ptr<luma::control>, modulus::error> root) -> std::expected<void, modulus::error>;
auto set_root(std::unique_ptr<luma::control> root) -> std::expected<void, modulus::error>;
template <typename CONTROL>
requires (std::is_base_of_v<luma::control, CONTROL> && !std::is_same_v<luma::control, CONTROL>)
auto set_root(std::unique_ptr<CONTROL> root) -> std::expected<void, modulus::error>;
auto show_dialog(luma::dialog_options options) -> std::expected<void, modulus::error>;
auto close_dialog(luma::dialog_result result) -> std::expected<void, modulus::error>;
auto show_toast(luma::toast toast) -> std::expected<void, modulus::error>;
auto request_stop() -> void;
auto should_stop() const -> std::bool_t;
auto run() -> std::expected<void, modulus::error>;
private:
auto core_snapshot() const -> std::shared_ptr<luma::detail::application_core>;
mutable std::mutex core_mutex_{};
std::shared_ptr<luma::detail::application_core> core_{};
};
template <typename CONTROL>
requires (std::is_base_of_v<luma::control, CONTROL> && !std::is_same_v<luma::control, CONTROL>)
auto application::set_root(std::unique_ptr<CONTROL> root) -> std::expected<void, modulus::error> {
return set_root(std::unique_ptr<luma::control>{std::move(root)});
}
}
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#include <cstdint>
#include <expected>
#include <memory>
#include <mutex>
#include <string>
#include <unordered_map>
#include <modulus.hpp>
#include <luma/render.hpp>
namespace luma {
class renderer_resources {
public:
virtual ~renderer_resources() = default;
virtual auto load_texture(std::string const& path) -> std::expected<std::shared_ptr<luma::texture>, modulus::error> = 0;
};
class asset_cache {
public:
explicit asset_cache(luma::renderer_resources& resources);
auto texture(std::string const& path) -> std::expected<std::shared_ptr<luma::texture>, modulus::error>;
auto clear() -> void;
private:
struct in_flight_load;
luma::renderer_resources* resources_{};
mutable std::mutex mutex_{};
std::uint64_t generation_{};
std::unordered_map<std::string, std::shared_ptr<luma::texture>> textures_{};
std::unordered_map<std::string, std::shared_ptr<luma::asset_cache::in_flight_load>> loading_{};
};
}
+45
View File
@@ -0,0 +1,45 @@
#pragma once
#include <cstdint>
#include <expected>
#include <string>
#include <boost/json/value.hpp>
#include <modulus.hpp>
#include <luma/theme.hpp>
namespace luma {
struct display_config {
std::int32_t width{1024};
std::int32_t height{600};
std::int32_t target_fps{60};
std::int32_t idle_fps{15};
std::bool_t fullscreen{true};
};
struct touch_config {
std::int32_t max_points{5};
};
struct application_config {
std::string title{"Luma"};
luma::display_config display{};
luma::touch_config touch{};
luma::theme theme{luma::default_theme()};
};
auto validate(luma::display_config const& config) -> std::expected<void, modulus::error>;
auto validate(luma::touch_config const& config) -> std::expected<void, modulus::error>;
auto validate(luma::application_config const& config) -> std::expected<void, modulus::error>;
auto parse_display_config(boost::json::value const& value) -> std::expected<luma::display_config, modulus::error>;
auto parse_touch_config(boost::json::value const& value) -> std::expected<luma::touch_config, modulus::error>;
auto parse_application_config(boost::json::value const& value) -> std::expected<luma::application_config, modulus::error>;
auto load_application_config(std::string const& path) -> std::expected<luma::application_config, modulus::error>;
auto to_json(luma::display_config const& config) -> boost::json::value;
auto to_json(luma::touch_config const& config) -> boost::json::value;
auto to_json(luma::application_config const& config) -> boost::json::value;
}
+40
View File
@@ -0,0 +1,40 @@
#pragma once
#include <functional>
#include <modulus.hpp>
#include <luma/geometry.hpp>
namespace luma {
class draw_context;
struct theme;
struct touch_event;
class control {
public:
virtual ~control();
virtual auto measure(luma::draw_context& context, luma::theme const& theme, luma::constraints limits) -> luma::size = 0;
virtual auto arrange(luma::rect bounds) -> void;
virtual auto draw(luma::draw_context& context, luma::theme const& theme) const -> void = 0;
virtual auto handle_touch(luma::touch_event const& event) -> std::bool_t;
virtual auto invalidate_layout() -> void;
virtual auto bind_layout_invalidator(std::function<void()> invalidator) -> void;
virtual auto focusable() const -> std::bool_t;
virtual auto set_focused(std::bool_t value) -> void;
auto focused() const -> std::bool_t;
auto set_enabled(std::bool_t value) -> void;
auto enabled() const -> std::bool_t;
auto bounds() const -> luma::rect;
protected:
auto notify_layout_changed() const -> void;
auto layout_invalidator() const -> std::function<void()>;
private:
luma::rect bounds_{};
std::function<void()> layout_invalidator_{};
std::bool_t focused_{false};
std::bool_t enabled_{true};
};
}
+185
View File
@@ -0,0 +1,185 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <expected>
#include <functional>
#include <memory>
#include <mutex>
#include <string>
#include <string_view>
#include <type_traits>
#include <utility>
#include <vector>
#include <modulus.hpp>
#include <luma/control.hpp>
#include <luma/detail/errors.hpp>
#include <luma/render.hpp>
namespace luma {
enum class direction {
ROW,
COLUMN,
};
enum class alignment {
START,
CENTER,
END,
STRETCH,
};
enum class text_input_mode {
TEXT,
INTEGER,
DECIMAL,
};
enum class image_fit {
CONTAIN,
COVER,
FILL,
CENTER,
};
enum class keyboard_key_kind {
CHARACTER,
BACKSPACE,
SPACE,
ENTER,
};
struct keyboard_key {
std::string label{};
std::string value{};
luma::keyboard_key_kind kind{luma::keyboard_key_kind::CHARACTER};
};
struct keyboard_layout {
std::vector<std::vector<luma::keyboard_key>> rows{};
};
struct text_field_options {
std::string text{};
std::string placeholder{};
luma::text_input_mode mode{luma::text_input_mode::TEXT};
std::bool_t password{false};
std::size_t max_length{256};
};
class text_input_state {
public:
struct layout_listener {
std::weak_ptr<void> lifetime{};
std::function<void()> callback{};
};
text_input_state(luma::text_field_options options, std::function<void(std::string)> on_change);
auto snapshot() const -> luma::text_field_options;
auto text() const -> std::string;
auto focused() const -> std::bool_t;
auto set_focused(std::bool_t value) -> void;
auto version() const -> std::uint64_t;
auto add_layout_listener(std::weak_ptr<void> lifetime, std::function<void()> callback) -> void;
auto append(std::string_view value) -> std::expected<void, modulus::error>;
auto backspace() -> std::expected<void, modulus::error>;
auto set_text(std::string value) -> std::expected<void, modulus::error>;
private:
mutable std::mutex mutex_{};
luma::text_field_options options_{};
std::function<void(std::string)> on_change_{};
std::vector<luma::text_input_state::layout_listener> layout_listeners_{};
std::bool_t focused_{false};
std::uint64_t version_{};
};
auto default_text_keyboard() -> luma::keyboard_layout;
auto numeric_keyboard() -> luma::keyboard_layout;
auto make_text_input(luma::text_field_options options, std::function<void(std::string)> on_change) -> std::expected<std::shared_ptr<luma::text_input_state>, modulus::error>;
template <typename... CONTROLS>
auto children(CONTROLS&&... controls) -> std::expected<std::vector<std::unique_ptr<luma::control>>, modulus::error>;
auto label(std::string text) -> std::expected<std::unique_ptr<luma::control>, modulus::error>;
auto button(std::string text, std::function<void()> on_click) -> std::expected<std::unique_ptr<luma::control>, modulus::error>;
auto text_field(luma::text_field_options options, std::function<void(std::string)> on_change) -> std::expected<std::unique_ptr<luma::control>, modulus::error>;
auto text_field(std::shared_ptr<luma::text_input_state> input) -> std::expected<std::unique_ptr<luma::control>, modulus::error>;
auto soft_keyboard(luma::keyboard_layout layout, std::shared_ptr<luma::text_input_state> input, std::function<void()> on_enter = {}) -> std::expected<std::unique_ptr<luma::control>, modulus::error>;
auto soft_keyboard(luma::keyboard_layout layout, std::function<void(std::string)> on_text, std::function<void()> on_enter = {}) -> std::expected<std::unique_ptr<luma::control>, modulus::error>;
auto panel(std::expected<std::vector<std::unique_ptr<luma::control>>, modulus::error> children = luma::children()) -> std::expected<std::unique_ptr<luma::control>, modulus::error>;
auto panel(std::vector<std::unique_ptr<luma::control>> children) -> std::expected<std::unique_ptr<luma::control>, modulus::error>;
auto spacer(luma::size value) -> std::expected<std::unique_ptr<luma::control>, modulus::error>;
auto progress_bar(std::float32_t value) -> std::expected<std::unique_ptr<luma::control>, modulus::error>;
auto image(std::shared_ptr<luma::texture> texture, luma::image_fit fit = luma::image_fit::CONTAIN) -> std::expected<std::unique_ptr<luma::control>, modulus::error>;
auto row(std::expected<std::vector<std::unique_ptr<luma::control>>, modulus::error> children, std::float32_t gap = 8.0F, luma::alignment align = luma::alignment::CENTER) -> std::expected<std::unique_ptr<luma::control>, modulus::error>;
auto row(std::vector<std::unique_ptr<luma::control>> children, std::float32_t gap = 8.0F, luma::alignment align = luma::alignment::CENTER) -> std::expected<std::unique_ptr<luma::control>, modulus::error>;
auto column(std::expected<std::vector<std::unique_ptr<luma::control>>, modulus::error> children, std::float32_t gap = 8.0F, luma::alignment align = luma::alignment::STRETCH) -> std::expected<std::unique_ptr<luma::control>, modulus::error>;
auto column(std::vector<std::unique_ptr<luma::control>> children, std::float32_t gap = 8.0F, luma::alignment align = luma::alignment::STRETCH) -> std::expected<std::unique_ptr<luma::control>, modulus::error>;
auto stack(std::expected<std::vector<std::unique_ptr<luma::control>>, modulus::error> children) -> std::expected<std::unique_ptr<luma::control>, modulus::error>;
auto stack(std::vector<std::unique_ptr<luma::control>> children) -> std::expected<std::unique_ptr<luma::control>, modulus::error>;
auto scroll_view(std::expected<std::unique_ptr<luma::control>, modulus::error> child, std::bool_t vertical = true, std::bool_t horizontal = false) -> std::expected<std::unique_ptr<luma::control>, modulus::error>;
auto scroll_view(std::unique_ptr<luma::control> child, std::bool_t vertical = true, std::bool_t horizontal = false) -> std::expected<std::unique_ptr<luma::control>, modulus::error>;
template <typename CONTROL>
requires (std::is_base_of_v<luma::control, CONTROL> && !std::is_same_v<luma::control, CONTROL>)
auto scroll_view(std::unique_ptr<CONTROL> child, std::bool_t vertical = true, std::bool_t horizontal = false) -> std::expected<std::unique_ptr<luma::control>, modulus::error>;
auto list(std::expected<std::vector<std::unique_ptr<luma::control>>, modulus::error> children, std::float32_t gap = 4.0F) -> std::expected<std::unique_ptr<luma::control>, modulus::error>;
auto list(std::vector<std::unique_ptr<luma::control>> children, std::float32_t gap = 4.0F) -> std::expected<std::unique_ptr<luma::control>, modulus::error>;
}
namespace luma::detail {
template <typename CONTROL>
requires std::is_base_of_v<luma::control, CONTROL>
auto normalize_child(std::unique_ptr<CONTROL> child) -> std::expected<std::unique_ptr<luma::control>, modulus::error> {
if (child == nullptr) {
return luma::detail::fail<std::unique_ptr<luma::control>>(luma::detail::error_invalid_argument, "child control must not be null");
}
return modulus::success(std::unique_ptr<luma::control>{std::move(child)});
}
template <typename CONTROL>
requires std::is_base_of_v<luma::control, CONTROL>
auto normalize_child(std::expected<std::unique_ptr<CONTROL>, modulus::error> child) -> std::expected<std::unique_ptr<luma::control>, modulus::error> {
if (!child) {
return modulus::fail<std::unique_ptr<luma::control>>(child.error());
}
return luma::detail::normalize_child(std::move(*child));
}
}
template <typename CONTROL>
requires (std::is_base_of_v<luma::control, CONTROL> && !std::is_same_v<luma::control, CONTROL>)
auto luma::scroll_view(std::unique_ptr<CONTROL> child, std::bool_t vertical, std::bool_t horizontal) -> std::expected<std::unique_ptr<luma::control>, modulus::error> {
return luma::scroll_view(std::unique_ptr<luma::control>{std::move(child)}, vertical, horizontal);
}
template <typename... CONTROLS>
auto luma::children(CONTROLS&&... controls) -> std::expected<std::vector<std::unique_ptr<luma::control>>, modulus::error> {
auto result = std::vector<std::unique_ptr<luma::control>>{};
result.reserve(sizeof...(controls));
auto append = [&result](auto&& input) -> std::expected<void, modulus::error> {
auto child = luma::detail::normalize_child(std::forward<decltype(input)>(input));
if (!child) {
return modulus::fail<void>(child.error());
}
result.push_back(std::move(*child));
return {};
};
auto status = std::expected<void, modulus::error>{};
([&] {
if (status) {
status = append(std::forward<CONTROLS>(controls));
}
}(), ...);
if (!status) {
return modulus::fail<std::vector<std::unique_ptr<luma::control>>>(status.error());
}
return modulus::success(std::move(result));
}
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include <cstdint>
#include <expected>
#include <string>
#include <utility>
#include <modulus.hpp>
namespace luma::detail {
constexpr std::int32_t error_invalid_argument{1};
constexpr std::int32_t error_not_found{2};
constexpr std::int32_t error_unavailable{3};
constexpr std::int32_t error_timeout{4};
constexpr std::int32_t error_internal{5};
auto make_error(std::int32_t code, std::string message, modulus::source_location location = modulus::source_location(__builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) -> modulus::error;
template <typename VALUE>
auto fail(std::int32_t code, std::string message, modulus::source_location location = modulus::source_location(__builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) -> std::expected<VALUE, modulus::error>;
}
template <typename VALUE>
auto luma::detail::fail(std::int32_t code, std::string message, modulus::source_location location) -> std::expected<VALUE, modulus::error> {
return modulus::fail<VALUE>(luma::detail::make_error(code, std::move(message), location));
}
+70
View File
@@ -0,0 +1,70 @@
#pragma once
#include <algorithm>
#include <limits>
#include <modulus.hpp>
namespace luma {
struct point {
std::float32_t x{};
std::float32_t y{};
};
struct size {
std::float32_t width{};
std::float32_t height{};
};
struct rect {
std::float32_t x{};
std::float32_t y{};
std::float32_t width{};
std::float32_t height{};
};
struct edge_insets {
std::float32_t left{};
std::float32_t top{};
std::float32_t right{};
std::float32_t bottom{};
};
struct constraints {
luma::size min{};
luma::size max{.width = std::numeric_limits<std::float32_t>::max(), .height = std::numeric_limits<std::float32_t>::max()};
};
constexpr auto right(luma::rect value) -> std::float32_t;
constexpr auto bottom(luma::rect value) -> std::float32_t;
constexpr auto contains(luma::rect area, luma::point value) -> std::bool_t;
constexpr auto inset(luma::rect area, luma::edge_insets value) -> luma::rect;
constexpr auto clamp(luma::size value, luma::constraints limits) -> luma::size;
constexpr auto deflate(luma::constraints limits, luma::edge_insets value) -> luma::constraints;
}
constexpr auto luma::right(luma::rect value) -> std::float32_t {
return value.x + value.width;
}
constexpr auto luma::bottom(luma::rect value) -> std::float32_t {
return value.y + value.height;
}
constexpr auto luma::contains(luma::rect area, luma::point value) -> std::bool_t {
return value.x >= area.x && value.x <= luma::right(area) && value.y >= area.y && value.y <= luma::bottom(area);
}
constexpr auto luma::inset(luma::rect area, luma::edge_insets value) -> luma::rect {
return {.x = area.x + value.left, .y = area.y + value.top, .width = std::max(std::float32_t{}, area.width - value.left - value.right), .height = std::max(std::float32_t{}, area.height - value.top - value.bottom)};
}
constexpr auto luma::clamp(luma::size value, luma::constraints limits) -> luma::size {
return {.width = std::clamp(value.width, limits.min.width, limits.max.width), .height = std::clamp(value.height, limits.min.height, limits.max.height)};
}
constexpr auto luma::deflate(luma::constraints limits, luma::edge_insets value) -> luma::constraints {
return {.min = {.width = std::max(std::float32_t{}, limits.min.width - value.left - value.right), .height = std::max(std::float32_t{}, limits.min.height - value.top - value.bottom)}, .max = {.width = std::max(std::float32_t{}, limits.max.width - value.left - value.right), .height = std::max(std::float32_t{}, limits.max.height - value.top - value.bottom)}};
}
+29
View File
@@ -0,0 +1,29 @@
#pragma once
#include <chrono>
#include <cstdint>
#include <vector>
#include <luma/geometry.hpp>
namespace luma {
enum class touch_phase {
DOWN,
MOVE,
UP,
CANCEL,
};
struct touch_point {
std::int32_t id{};
luma::point position{};
luma::touch_phase phase{luma::touch_phase::MOVE};
};
struct touch_event {
std::vector<luma::touch_point> points{};
std::chrono::steady_clock::time_point time{std::chrono::steady_clock::now()};
};
}
+67
View File
@@ -0,0 +1,67 @@
#pragma once
#include <chrono>
#include <cstdint>
#include <deque>
#include <expected>
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include <modulus.hpp>
#include <luma/control.hpp>
namespace luma {
enum class dialog_result {
NONE,
ACCEPTED,
CANCELLED,
};
struct dialog_options {
std::string title{};
std::unique_ptr<luma::control> body{};
std::string accept_text{"OK"};
std::string cancel_text{"Cancel"};
std::function<void(luma::dialog_result)> on_close{};
};
struct toast {
std::string message{};
std::chrono::milliseconds duration{2500};
};
class overlay_host final : public luma::control {
public:
auto show_dialog(luma::dialog_options options) -> std::expected<void, modulus::error>;
auto close_dialog(luma::dialog_result result) -> std::expected<void, modulus::error>;
auto show_toast(luma::toast toast) -> std::expected<void, modulus::error>;
auto tick(std::chrono::steady_clock::duration delta) -> std::bool_t;
auto measure(luma::draw_context& context, luma::theme const& theme, luma::constraints limits) -> luma::size override;
auto arrange(luma::rect bounds) -> void override;
auto draw(luma::draw_context& context, luma::theme const& theme) const -> void override;
auto handle_touch(luma::touch_event const& event) -> std::bool_t override;
auto invalidate_layout() -> void override;
auto bind_layout_invalidator(std::function<void()> invalidator) -> void override;
private:
friend auto overlays(std::unique_ptr<luma::control> content) -> std::expected<std::unique_ptr<luma::overlay_host>, modulus::error>;
explicit overlay_host(std::unique_ptr<luma::control> content);
auto begin_traversal() const -> void;
auto end_traversal() const -> void;
auto retire_dialog_body(std::unique_ptr<luma::control> body) -> void;
auto flush_retired_dialog_bodies() const -> void;
std::unique_ptr<luma::control> content_{};
std::optional<luma::dialog_options> dialog_{};
std::deque<luma::toast> toasts_{};
std::chrono::steady_clock::duration toast_elapsed_{};
mutable std::vector<std::unique_ptr<luma::control>> retired_dialog_bodies_{};
mutable std::uint32_t traversal_depth_{};
};
auto overlays(std::unique_ptr<luma::control> content) -> std::expected<std::unique_ptr<luma::overlay_host>, modulus::error>;
}
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#include <cstdint>
#include <string_view>
#include <modulus.hpp>
#include <luma/geometry.hpp>
#include <luma/theme.hpp>
namespace luma {
struct texture_info {
std::int32_t width{};
std::int32_t height{};
};
class texture {
public:
virtual ~texture() = default;
virtual auto info() const -> luma::texture_info = 0;
};
class draw_context {
public:
virtual ~draw_context() = default;
virtual auto fill_rect(luma::rect bounds, luma::color color) -> void = 0;
virtual auto stroke_rect(luma::rect bounds, std::float32_t thickness, luma::color color) -> void = 0;
virtual auto stroke_line(luma::point from, luma::point to, std::float32_t thickness, luma::color color) -> void = 0;
virtual auto fill_ellipse(luma::rect bounds, luma::color color) -> void = 0;
virtual auto stroke_ellipse(luma::rect bounds, std::float32_t thickness, luma::color color) -> void = 0;
virtual auto draw_text(std::string_view text, luma::point origin, luma::text_style const& style, luma::color color) -> void = 0;
virtual auto draw_texture(luma::texture const& texture, luma::rect source, luma::rect destination, luma::color tint) -> void = 0;
virtual auto push_clip(luma::rect bounds) -> void = 0;
virtual auto pop_clip() -> void = 0;
virtual auto measure_text(std::string_view text, luma::text_style const& style) const -> luma::size = 0;
};
}
+40
View File
@@ -0,0 +1,40 @@
#pragma once
#include <expected>
#include <functional>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
#include <modulus.hpp>
#include <luma/control.hpp>
namespace luma {
class router {
public:
auto register_route(std::string name, std::function<std::expected<std::unique_ptr<luma::control>, modulus::error>()> factory) -> std::expected<void, modulus::error>;
auto set_retire_handler(std::function<void(std::unique_ptr<luma::control>)> handler) -> void;
auto replace(std::string const& name) -> std::expected<void, modulus::error>;
auto push(std::string const& name) -> std::expected<void, modulus::error>;
auto pop() -> std::expected<void, modulus::error>;
auto clear_stack() -> void;
auto current() -> luma::control*;
auto current() const -> luma::control const*;
auto current_route() const -> std::string const&;
private:
struct entry {
std::string name{};
std::unique_ptr<luma::control> view{};
};
auto retire(std::unique_ptr<luma::control> view) -> void;
auto retire_stack() -> void;
std::unordered_map<std::string, std::function<std::expected<std::unique_ptr<luma::control>, modulus::error>()>> routes_{};
std::vector<luma::router::entry> stack_{};
std::string empty_route_{};
std::function<void(std::unique_ptr<luma::control>)> retire_handler_{};
};
}
+49
View File
@@ -0,0 +1,49 @@
#pragma once
#include <cstdint>
#include <expected>
#include <string>
#include <boost/json/value.hpp>
#include <modulus.hpp>
namespace luma {
struct color {
std::uint8_t r{};
std::uint8_t g{};
std::uint8_t b{};
std::uint8_t a{255};
};
struct text_style {
std::string font{"default"};
std::float32_t size{24.0F};
std::float32_t line_height{30.0F};
};
struct theme {
luma::color background{18, 19, 22, 255};
luma::color surface{32, 34, 38, 255};
luma::color surface_active{46, 94, 170, 255};
luma::color text{242, 244, 248, 255};
luma::color text_muted{170, 176, 186, 255};
luma::color border{82, 88, 99, 255};
luma::color accent{84, 160, 255, 255};
luma::text_style body{};
luma::text_style title{.font = "default", .size = 32.0F, .line_height = 40.0F};
std::float32_t radius{4.0F};
std::float32_t spacing{8.0F};
};
auto default_theme() -> luma::theme;
auto validate(luma::text_style const& style) -> std::expected<void, modulus::error>;
auto validate(luma::theme const& theme) -> std::expected<void, modulus::error>;
auto parse_color(boost::json::value const& value) -> std::expected<luma::color, modulus::error>;
auto parse_theme(boost::json::value const& value) -> std::expected<luma::theme, modulus::error>;
auto to_json(luma::color const& color) -> boost::json::value;
auto to_json(luma::text_style const& style) -> boost::json::value;
auto to_json(luma::theme const& theme) -> boost::json::value;
}
+94
View File
@@ -0,0 +1,94 @@
#pragma once
#include <cstdint>
#include <expected>
#include <functional>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
#include <modulus.hpp>
#include <luma/control.hpp>
#include <luma/geometry.hpp>
namespace luma {
class window_manager;
struct window_options {
std::string title{};
luma::rect bounds{};
std::bool_t modal{false};
std::bool_t decorated{true};
std::bool_t visible{true};
};
class window {
public:
window(luma::window const&) = delete;
window(luma::window&&) noexcept = default;
auto operator=(luma::window const&) -> luma::window& = delete;
auto operator=(luma::window&&) noexcept -> luma::window& = default;
auto id() const -> std::uint64_t;
auto options() const -> luma::window_options const&;
auto set_bounds(luma::rect bounds) -> std::expected<void, modulus::error>;
auto set_visible(std::bool_t value) -> void;
auto visible() const -> std::bool_t;
auto content() -> luma::control*;
auto content() const -> luma::control const*;
private:
friend class luma::window_manager;
window(std::uint64_t id, luma::window_options options, std::unique_ptr<luma::control> content);
std::uint64_t id_{};
luma::window_options options_{};
std::unique_ptr<luma::control> content_{};
};
class window_manager final : public luma::control {
public:
auto open(luma::window_options options, std::unique_ptr<luma::control> content) -> std::expected<std::uint64_t, modulus::error>;
auto close(std::uint64_t id) -> std::expected<void, modulus::error>;
auto bring_to_front(std::uint64_t id) -> std::expected<void, modulus::error>;
auto set_bounds(std::uint64_t id, luma::rect bounds) -> std::expected<void, modulus::error>;
auto set_visible(std::uint64_t id, std::bool_t visible) -> std::expected<void, modulus::error>;
auto find(std::uint64_t id) -> luma::window*;
auto clear() -> void;
auto measure(luma::draw_context& context, luma::theme const& theme, luma::constraints limits) -> luma::size override;
auto arrange(luma::rect bounds) -> void override;
auto draw(luma::draw_context& context, luma::theme const& theme) const -> void override;
auto handle_touch(luma::touch_event const& event) -> std::bool_t override;
auto invalidate_layout() -> void override;
auto bind_layout_invalidator(std::function<void()> invalidator) -> void override;
private:
enum class pending_kind {
OPEN,
CLOSE,
BRING_TO_FRONT,
SET_BOUNDS,
SET_VISIBLE,
CLEAR,
};
struct pending_operation {
luma::window_manager::pending_kind kind{luma::window_manager::pending_kind::CLEAR};
std::uint64_t id{};
std::unique_ptr<luma::window> window{};
luma::rect bounds{};
std::bool_t visible{};
};
auto contains_effective(std::uint64_t id) const -> std::bool_t;
auto close_now(std::uint64_t id) const -> void;
auto bring_to_front_now(std::uint64_t id) const -> void;
auto set_bounds_now(std::uint64_t id, luma::rect bounds) const -> void;
auto set_visible_now(std::uint64_t id, std::bool_t visible) const -> void;
auto apply_pending_operations() const -> void;
std::uint64_t next_id_{1};
mutable std::float32_t title_line_height_{30.0F};
mutable std::vector<std::unique_ptr<luma::window>> windows_{};
mutable std::unordered_map<std::int32_t, std::uint64_t> active_touches_{};
mutable std::vector<luma::window_manager::pending_operation> pending_operations_{};
mutable std::uint32_t mutation_depth_{};
};
}
+11
View File
@@ -0,0 +1,11 @@
{
"package": {
"name": "luma"
},
"tooling": {
"remote": {
"name": "khodo",
"url": "https://git.thyme-labs.com/api/packages/khodo/conan"
}
}
}
Executable
+367
View File
@@ -0,0 +1,367 @@
#!/usr/bin/env python3
import json
import os
import pathlib
import shlex
import subprocess
import sys
PACKAGE_REF = "modulus/latest"
CONAN_VERSION = "2.26.2"
FETCH_IMAGE = "modulus-fetch:dev"
COMMANDS = (
"configure",
"build",
"test",
"package",
"lsp",
)
COMMON_DIR = pathlib.Path("build") / "build_common"
TOOLS_DIR = pathlib.Path("build") / "build_tools"
RUNTIME_DIR = COMMON_DIR / "modulus"
CONAN_HOME_DIR = COMMON_DIR / "conan_cache"
DEFAULT_REMOTE_NAME = "khodo"
DEFAULT_REMOTE_URL = "https://git.thyme-labs.com/api/packages/khodo/conan"
DEFAULT_DOCKER_MARKER = "/opt/modulus-build-image"
def repo_root():
root = pathlib.Path(__file__).resolve().parent
if not (root / "modulus.json").is_file():
raise SystemExit("modulus.py must be run from a Modulus-managed repository root")
return root
def read_json(path):
try:
data = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as error:
raise SystemExit(f"failed to read {path}: {error}") from error
if not isinstance(data, dict):
raise SystemExit(f"{path} must contain a JSON object")
return data
def remote_config(root):
config = read_json(root / "modulus.json")
remote = config.get(
"tooling",
{},
).get("remote", {})
if not isinstance(remote, dict):
remote = {}
return (
str(remote.get("name") or DEFAULT_REMOTE_NAME),
str(remote.get("url") or DEFAULT_REMOTE_URL),
)
def runtime_ready(root):
return (
(root / TOOLS_DIR / "configure.py").is_file()
and (root / TOOLS_DIR / "lsp.py").is_file()
and (root / RUNTIME_DIR / "modulus" / "cli.py").is_file()
and (root / RUNTIME_DIR / "modulus" / "export.py").is_file()
)
def optional_credentials():
user = os.environ.get("CONAN_USER")
token = os.environ.get("CONAN_TOKEN")
if bool(user) != bool(token):
raise SystemExit("CONAN_USER and CONAN_TOKEN must either both be set or both be unset")
return user, token
def env_flag(name):
return os.environ.get(name, "").lower() in {"1", "true", "yes", "on"}
def docker_marker(root):
config = read_json(root / "modulus.json")
marker = config.get(
"tooling",
{},
).get("docker", {}).get("marker", DEFAULT_DOCKER_MARKER)
return pathlib.Path(str(marker))
def direct_mode(root):
return env_flag("MODULUS_DIRECT") or docker_marker(root).exists()
def run(command, input_text=None, capture=False, display_command=None, env=None, check=True):
visible = display_command if display_command is not None else command
print("+", " ".join(shlex.quote(str(part)) for part in visible))
completed = subprocess.run(
[str(part) for part in command],
input=input_text,
stdout=subprocess.PIPE if capture else None,
stderr=subprocess.PIPE if capture else None,
text=True,
check=False,
env=env,
)
if check and completed.returncode != 0:
if capture and completed.stdout:
print(completed.stdout, end="")
if capture and completed.stderr:
print(completed.stderr, end="", file=sys.stderr)
raise SystemExit(completed.returncode)
return completed
def fetch_dockerfile():
return f"""FROM debian:bookworm-slim
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \\
&& apt-get install -y --no-install-recommends \\
ca-certificates \\
python3 \\
python3-venv \\
&& rm -rf /var/lib/apt/lists/*
RUN python3 -m venv /opt/modulus-python
ENV PATH=/opt/modulus-python/bin:$PATH
RUN python -m pip install --no-cache-dir conan=={CONAN_VERSION}
WORKDIR /workspace
"""
def ensure_fetch_image():
inspected = subprocess.run(
[
"docker",
"image",
"inspect",
FETCH_IMAGE,
],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=False,
)
if inspected.returncode == 0:
return
run(
[
"docker",
"build",
"--tag",
FETCH_IMAGE,
"-",
],
input_text=fetch_dockerfile(),
)
def in_container():
return pathlib.Path("/.dockerenv").exists()
def docker_workspace_args(root):
if in_container():
container = os.environ.get("HOSTNAME", "").strip()
if not container:
raise SystemExit("HOSTNAME is required to share the CI workspace with the Modulus fetch container")
return [
"--volumes-from",
container,
]
return [
"--volume",
f"{root}:{root}",
]
def fetch_env(root):
env = os.environ.copy()
env["CONAN_HOME"] = str(root / CONAN_HOME_DIR)
return env
def run_fetch(root, command, capture=False, display_command=None, check=True):
if direct_mode(root):
return run(command, capture=capture, display_command=display_command, env=fetch_env(root), check=check)
ensure_fetch_image()
docker_command = [
"docker",
"run",
"--rm",
"--interactive",
"--user",
f"{os.getuid()}:{os.getgid()}",
"--env",
"HOME=/tmp",
"--env",
"CONAN_HOME",
"--env",
"CONAN_USER",
"--env",
"CONAN_TOKEN",
*docker_workspace_args(root),
"--workdir",
str(root),
FETCH_IMAGE,
*command,
]
return run(docker_command, capture=capture, display_command=display_command, env=fetch_env(root), check=check)
def configure_remote(root, remote_name, remote_url):
user, token = optional_credentials()
run_fetch(
root,
[
"conan",
"remote",
"add",
"-f",
remote_name,
remote_url,
],
)
if not user and not token:
return
run_fetch(
root,
[
"conan",
"remote",
"login",
remote_name,
user,
"-p",
token,
],
display_command=[
"conan",
"remote",
"login",
remote_name,
user,
"-p",
"<hidden>",
],
)
def package_refs(data):
refs = []
for location_data in data.values():
if not isinstance(location_data, dict):
continue
for ref, ref_data in location_data.items():
if not ref.startswith(PACKAGE_REF) or not isinstance(ref_data, dict):
continue
revisions = ref_data.get(
"revisions",
{},
)
if not isinstance(revisions, dict):
continue
for recipe_revision, revision_data in revisions.items():
if not isinstance(revision_data, dict):
continue
packages = revision_data.get(
"packages",
{},
)
if not isinstance(packages, dict):
continue
for package_id, package_data in packages.items():
package_revisions = package_data.get(
"revisions",
{},
)if isinstance(package_data, dict) else {}
if isinstance(package_revisions, dict) and package_revisions:
for package_revision in package_revisions:
refs.append(f"{PACKAGE_REF}#{recipe_revision}:{package_id}#{package_revision}")
else:
refs.append(f"{PACKAGE_REF}#{recipe_revision}:{package_id}")
return sorted(refs)
def remove_cached_modulus(root):
run_fetch(
root,
[
"conan",
"remove",
PACKAGE_REF + ":*",
"-c",
],
check=False,
)
def download_modulus(root, remote_name):
remove_cached_modulus(root)
run_fetch(
root,
[
"conan",
"download",
PACKAGE_REF + ":*",
"-r",
remote_name,
],
)
completed = run_fetch(
root,
[
"conan",
"list",
PACKAGE_REF + ":*",
"-c",
"-f",
"json",
],
capture=True,
)
refs = package_refs(json.loads(completed.stdout))
if not refs:
raise SystemExit(f"no downloaded {PACKAGE_REF} package found in the Conan cache")
package_ref = refs[-1]
completed = run_fetch(
root,
[
"conan",
"cache",
"path",
package_ref,
],
capture=True,
)
package_root = pathlib.Path(completed.stdout.strip())
if not (package_root / "modulus" / "export.py").is_file() or not (package_root / "tools").is_dir():
raise SystemExit(f"downloaded {PACKAGE_REF} package does not contain Modulus export files: {package_root}")
return package_root
def materialize_runtime(root):
remote_name, remote_url = remote_config(root)
configure_remote(root, remote_name, remote_url)
package_root = download_modulus(root, remote_name)
sys.path.insert(0, str(package_root))
import modulus.export
modulus.export.sync_project(root, package_root)
def ensure_runtime(root):
if runtime_ready(root):
return
materialize_runtime(root)
def delegate(command, argv):
root = repo_root()
ensure_runtime(root)
sys.path.insert(0, str(root / RUNTIME_DIR))
import modulus.cli
previous_argv = sys.argv
previous_cwd = pathlib.Path.cwd()
sys.argv = [
str(root / "modulus.py"),
*argv,
]
os.chdir(root)
try:
return modulus.cli.main(command)
finally:
os.chdir(previous_cwd)
sys.argv = previous_argv
def main(argv):
if not argv or argv[0] in ("-h", "--help"):
print("usage: python3 modulus.py " + "|".join(COMMANDS) + " ...")
return 0 if argv else 2
command = argv[0]
if command not in COMMANDS:
raise SystemExit(f"unknown Modulus command: {command}")
return delegate(command, argv[1:])
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
+193
View File
@@ -0,0 +1,193 @@
#include <luma/animation.hpp>
#include <algorithm>
#include <cmath>
#include <expected>
#include <exception>
#include <string>
#include <utility>
#include <modulus.hpp>
#include <luma/detail/errors.hpp>
namespace {
auto invalid(std::string message) -> std::expected<void, modulus::error> {
return luma::detail::fail<void>(luma::detail::error_invalid_argument, std::move(message));
}
template <typename VALUE>
auto invalid_value(std::string message) -> std::expected<VALUE, modulus::error> {
return luma::detail::fail<VALUE>(luma::detail::error_invalid_argument, std::move(message));
}
auto internal(std::string message) -> std::expected<void, modulus::error> {
return luma::detail::fail<void>(luma::detail::error_internal, std::move(message));
}
auto validate(luma::animation const& animation) -> std::expected<void, modulus::error> {
if (!std::isfinite(animation.from)) {
return invalid("animation from value must be finite");
}
if (!std::isfinite(animation.to)) {
return invalid("animation to value must be finite");
}
if (animation.duration.count() <= 0) {
return invalid("animation duration must be positive");
}
if (!animation.apply) {
return invalid("animation apply callback must not be empty");
}
return {};
}
auto callback_error(std::char_t const* action, std::exception const& error) -> std::expected<void, modulus::error> {
return internal(std::string{"animation "} + action + " callback failed: " + error.what());
}
auto callback_error(std::char_t const* action) -> std::expected<void, modulus::error> {
return internal(std::string{"animation "} + action + " callback failed with an unknown exception");
}
}
auto luma::interpolate(std::float32_t from, std::float32_t to, std::float32_t progress, luma::easing easing) -> std::expected<std::float32_t, modulus::error> {
if (!std::isfinite(from)) {
return invalid_value<std::float32_t>("interpolation from value must be finite");
}
if (!std::isfinite(to)) {
return invalid_value<std::float32_t>("interpolation to value must be finite");
}
if (!std::isfinite(progress)) {
return invalid_value<std::float32_t>("interpolation progress must be finite");
}
auto clamped = std::clamp(progress, std::float32_t{}, std::float32_t{1});
auto eased = clamped;
switch (easing) {
case luma::easing::LINEAR:
eased = clamped;
break;
case luma::easing::EASE_OUT:
eased = 1.0F - (1.0F - clamped) * (1.0F - clamped);
break;
case luma::easing::EASE_IN_OUT:
eased = clamped < 0.5F ? 2.0F * clamped * clamped : 1.0F - std::pow(-2.0F * clamped + 2.0F, 2.0F) / 2.0F;
break;
}
return modulus::success(from + (to - from) * eased);
}
auto luma::animation_graph::add(luma::animation animation) -> std::expected<void, modulus::error> {
if (auto status = validate(animation); !status) {
return status;
}
auto running = luma::animation_graph::running_animation{
.animation = std::move(animation),
.elapsed = {},
};
if (ticking_) {
pending_.push_back({
.kind = luma::animation_graph::pending_kind::ADD,
.animation = std::move(running),
});
} else {
animations_.push_back(std::move(running));
}
return {};
}
auto luma::animation_graph::tick(std::chrono::steady_clock::duration delta) -> std::expected<void, modulus::error> {
if (delta < std::chrono::steady_clock::duration::zero()) {
return invalid("animation tick delta must be non-negative");
}
if (ticking_) {
return internal("animation graph is already ticking");
}
auto active = std::move(animations_);
animations_.clear();
ticking_ = true;
clear_requested_ = false;
auto fail_tick = [this](std::expected<void, modulus::error> status) {
ticking_ = false;
clear_requested_ = false;
animations_.clear();
pending_.clear();
return status;
};
for (auto& running : active) {
running.elapsed += delta;
auto duration = std::chrono::duration_cast<std::chrono::steady_clock::duration>(running.animation.duration);
if (duration.count() <= 0) {
return fail_tick(invalid("animation duration must be positive"));
}
auto progress = static_cast<std::float32_t>(running.elapsed.count()) / static_cast<std::float32_t>(duration.count());
progress = std::clamp(progress, std::float32_t{}, std::float32_t{1});
if (!running.animation.apply) {
return fail_tick(invalid("animation apply callback must not be empty"));
}
auto interpolated = luma::interpolate(running.animation.from, running.animation.to, progress, running.animation.easing);
if (!interpolated) {
return fail_tick(modulus::fail<void>(interpolated.error()));
}
try {
running.animation.apply(*interpolated);
} catch (std::exception const& error) {
return fail_tick(callback_error("apply", error));
} catch (...) {
return fail_tick(callback_error("apply"));
}
if (clear_requested_) {
break;
}
if (progress >= 1.0F) {
if (running.animation.finished) {
try {
running.animation.finished();
} catch (std::exception const& error) {
return fail_tick(callback_error("finished", error));
} catch (...) {
return fail_tick(callback_error("finished"));
}
}
if (clear_requested_) {
break;
}
continue;
}
animations_.push_back(std::move(running));
}
ticking_ = false;
for (auto& operation : pending_) {
if (operation.kind == luma::animation_graph::pending_kind::CLEAR) {
animations_.clear();
} else {
animations_.push_back(std::move(operation.animation));
}
}
pending_.clear();
clear_requested_ = false;
return {};
}
auto luma::animation_graph::empty() const -> std::bool_t {
return animations_.empty();
}
auto luma::animation_graph::clear() -> void {
if (ticking_) {
pending_.push_back({
.kind = luma::animation_graph::pending_kind::CLEAR,
.animation = {},
});
clear_requested_ = true;
} else {
animations_.clear();
pending_.clear();
clear_requested_ = false;
}
}
+1264
View File
File diff suppressed because it is too large Load Diff
+126
View File
@@ -0,0 +1,126 @@
#include <luma/assets.hpp>
#include <cassert>
#include <condition_variable>
#include <cstdint>
#include <expected>
#include <exception>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <utility>
#include <modulus.hpp>
#include <modulus/log.hpp>
#include <luma/detail/errors.hpp>
struct luma::asset_cache::in_flight_load {
std::condition_variable ready{};
std::thread::id owner{};
std::expected<std::shared_ptr<luma::texture>, modulus::error> result{};
std::bool_t done{false};
};
luma::asset_cache::asset_cache(luma::renderer_resources& resources) : resources_{&resources} {
assert(resources_ != nullptr);
}
auto luma::asset_cache::texture(std::string const& path) -> std::expected<std::shared_ptr<luma::texture>, modulus::error> {
assert(resources_ != nullptr);
if (path.empty()) {
return luma::detail::fail<std::shared_ptr<luma::texture>>(luma::detail::error_invalid_argument, "texture path must not be empty");
}
auto generation = std::uint64_t{};
auto loading = std::shared_ptr<luma::asset_cache::in_flight_load>{};
{
auto lock = std::unique_lock{mutex_};
if (auto found = textures_.find(path); found != textures_.end()) {
assert(found->second != nullptr);
return modulus::success(found->second);
}
if (auto found = loading_.find(path); found != loading_.end()) {
loading = found->second;
assert(loading != nullptr);
if (loading->owner == std::this_thread::get_id()) {
return luma::detail::fail<std::shared_ptr<luma::texture>>(luma::detail::error_unavailable, "texture load reentered for path: " + path);
}
loading->ready.wait(lock, [&loading] {
return loading->done;
});
if (auto cached = textures_.find(path); cached != textures_.end()) {
assert(cached->second != nullptr);
return modulus::success(cached->second);
}
if (!loading->result) {
return modulus::fail<std::shared_ptr<luma::texture>>(loading->result.error());
}
assert(*loading->result != nullptr);
return modulus::success(*loading->result);
}
loading = std::make_shared<luma::asset_cache::in_flight_load>();
loading->owner = std::this_thread::get_id();
loading_.emplace(path, loading);
generation = generation_;
}
auto loaded = std::expected<std::shared_ptr<luma::texture>, modulus::error>{};
modulus::log_debug("luma texture loading: " + path);
try {
loaded = resources_->load_texture(path);
} catch (std::exception const& error) {
loaded = luma::detail::fail<std::shared_ptr<luma::texture>>(luma::detail::error_internal, std::string{"texture load failed: "} + error.what());
} catch (...) {
loaded = luma::detail::fail<std::shared_ptr<luma::texture>>(luma::detail::error_internal, "texture load failed with an unknown exception");
}
if (!loaded) {
modulus::log_debug("luma texture load failed: " + path);
auto lock = std::scoped_lock{mutex_};
loading->result = modulus::fail<std::shared_ptr<luma::texture>>(loaded.error());
loading->done = true;
loading_.erase(path);
loading->ready.notify_all();
return loaded;
}
if (*loaded == nullptr) {
modulus::log_debug("luma texture load returned null: " + path);
auto result = luma::detail::fail<std::shared_ptr<luma::texture>>(luma::detail::error_not_found, "texture load returned no texture: " + path);
auto lock = std::scoped_lock{mutex_};
loading->result = modulus::fail<std::shared_ptr<luma::texture>>(result.error());
loading->done = true;
loading_.erase(path);
loading->ready.notify_all();
return result;
}
auto texture = *loaded;
auto lock = std::scoped_lock{mutex_};
if (auto found = textures_.find(path); found != textures_.end()) {
assert(found->second != nullptr);
loading->result = modulus::success(found->second);
loading->done = true;
loading_.erase(path);
loading->ready.notify_all();
return modulus::success(found->second);
}
if (generation != generation_) {
loading->result = modulus::success(texture);
loading->done = true;
loading_.erase(path);
loading->ready.notify_all();
return modulus::success(std::move(texture));
}
auto inserted = textures_.emplace(path, std::move(texture));
assert(inserted.first->second != nullptr);
loading->result = modulus::success(inserted.first->second);
loading->done = true;
loading_.erase(path);
loading->ready.notify_all();
return modulus::success(inserted.first->second);
}
auto luma::asset_cache::clear() -> void {
assert(resources_ != nullptr);
auto lock = std::scoped_lock{mutex_};
++generation_;
textures_.clear();
}
+261
View File
@@ -0,0 +1,261 @@
#include <luma/config.hpp>
#include <cassert>
#include <cstdint>
#include <expected>
#include <limits>
#include <string>
#include <utility>
#include <boost/json/object.hpp>
#include <boost/json/value.hpp>
#include <modulus.hpp>
#include <luma/detail/errors.hpp>
#include <luma/theme.hpp>
namespace {
constexpr auto json_options = modulus::json_config::options{
.category = "luma",
.invalid_argument_code = luma::detail::error_invalid_argument,
.not_found_code = luma::detail::error_not_found
};
auto invalid_display(std::string message) -> std::expected<luma::display_config, modulus::error> {
assert(!message.empty());
return luma::detail::fail<luma::display_config>(luma::detail::error_invalid_argument, std::move(message));
}
auto invalid_touch(std::string message) -> std::expected<luma::touch_config, modulus::error> {
assert(!message.empty());
return luma::detail::fail<luma::touch_config>(luma::detail::error_invalid_argument, std::move(message));
}
auto invalid_application(std::string message) -> std::expected<luma::application_config, modulus::error> {
assert(!message.empty());
return luma::detail::fail<luma::application_config>(luma::detail::error_invalid_argument, std::move(message));
}
}
auto luma::validate(luma::display_config const& config) -> std::expected<void, modulus::error> {
if (config.width <= 0) {
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "display.width must be positive");
}
if (config.height <= 0) {
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "display.height must be positive");
}
if (config.target_fps <= 0) {
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "display.target_fps must be positive");
}
if (config.idle_fps <= 0) {
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "display.idle_fps must be positive");
}
if (config.idle_fps > config.target_fps) {
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "display.idle_fps must be less than or equal to display.target_fps");
}
return {};
}
auto luma::validate(luma::touch_config const& config) -> std::expected<void, modulus::error> {
if (config.max_points <= 0) {
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "touch.max_points must be positive");
}
return {};
}
auto luma::validate(luma::application_config const& config) -> std::expected<void, modulus::error> {
if (config.title.empty()) {
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "title must not be empty");
}
if (auto status = luma::validate(config.display); !status) {
return modulus::fail<void>(status.error());
}
if (auto status = luma::validate(config.touch); !status) {
return modulus::fail<void>(status.error());
}
if (auto status = luma::validate(config.theme); !status) {
return modulus::fail<void>(status.error());
}
return {};
}
auto luma::parse_display_config(boost::json::value const& value) -> std::expected<luma::display_config, modulus::error> {
auto object = modulus::json_config::require_object(value, "display", json_options);
if (!object) {
return invalid_display(std::string{object.error().message()});
}
auto unknown = modulus::json_config::reject_unknown_fields(**object, {
"width",
"height",
"target_fps",
"idle_fps",
"fullscreen",
}, "display", json_options);
if (!unknown) {
return invalid_display(std::string{unknown.error().message()});
}
auto config = luma::display_config{};
auto width = modulus::json_config::optional_int32_field(**object, "width", config.width, std::numeric_limits<std::int32_t>::min(), std::numeric_limits<std::int32_t>::max(), json_options);
auto height = modulus::json_config::optional_int32_field(**object, "height", config.height, std::numeric_limits<std::int32_t>::min(), std::numeric_limits<std::int32_t>::max(), json_options);
auto target_fps = modulus::json_config::optional_int32_field(**object, "target_fps", config.target_fps, std::numeric_limits<std::int32_t>::min(), std::numeric_limits<std::int32_t>::max(), json_options);
auto idle_fps = modulus::json_config::optional_int32_field(**object, "idle_fps", config.idle_fps, std::numeric_limits<std::int32_t>::min(), std::numeric_limits<std::int32_t>::max(), json_options);
auto fullscreen = modulus::json_config::optional_bool_field(**object, "fullscreen", config.fullscreen, json_options);
if (!width) {
return invalid_display(std::string{width.error().message()});
}
if (!height) {
return invalid_display(std::string{height.error().message()});
}
if (!target_fps) {
return invalid_display(std::string{target_fps.error().message()});
}
if (!idle_fps) {
return invalid_display(std::string{idle_fps.error().message()});
}
if (!fullscreen) {
return invalid_display(std::string{fullscreen.error().message()});
}
config.width = *width;
config.height = *height;
config.target_fps = *target_fps;
config.idle_fps = *idle_fps;
config.fullscreen = *fullscreen;
if (auto status = luma::validate(config); !status) {
return invalid_display(std::string{status.error().message()});
}
return modulus::success(config);
}
auto luma::parse_touch_config(boost::json::value const& value) -> std::expected<luma::touch_config, modulus::error> {
auto object = modulus::json_config::require_object(value, "touch", json_options);
if (!object) {
return invalid_touch(std::string{object.error().message()});
}
auto unknown = modulus::json_config::reject_unknown_fields(**object, {"max_points"}, "touch", json_options);
if (!unknown) {
return invalid_touch(std::string{unknown.error().message()});
}
auto config = luma::touch_config{};
auto max_points = modulus::json_config::optional_int32_field(**object, "max_points", config.max_points, std::numeric_limits<std::int32_t>::min(), std::numeric_limits<std::int32_t>::max(), json_options);
if (!max_points) {
return invalid_touch(std::string{max_points.error().message()});
}
config.max_points = *max_points;
if (auto status = luma::validate(config); !status) {
return invalid_touch(std::string{status.error().message()});
}
return modulus::success(config);
}
auto luma::parse_application_config(boost::json::value const& value) -> std::expected<luma::application_config, modulus::error> {
auto object = modulus::json_config::require_object(value, "application config", json_options);
if (!object) {
return invalid_application(std::string{object.error().message()});
}
auto unknown = modulus::json_config::reject_unknown_fields(**object, {
"title",
"display",
"touch",
"theme",
}, "application config", json_options);
if (!unknown) {
return invalid_application(std::string{unknown.error().message()});
}
auto config = luma::application_config{};
auto title = modulus::json_config::optional_string_field(**object, "title", config.title, json_options);
if (!title) {
return invalid_application(std::string{title.error().message()});
}
config.title = *title;
if (auto const* display = (*object)->if_contains("display")) {
auto parsed = luma::parse_display_config(*display);
if (!parsed) {
return invalid_application(std::string{parsed.error().message()});
}
config.display = *parsed;
}
if (auto const* touch = (*object)->if_contains("touch")) {
auto parsed = luma::parse_touch_config(*touch);
if (!parsed) {
return invalid_application(std::string{parsed.error().message()});
}
config.touch = *parsed;
}
if (auto const* theme = (*object)->if_contains("theme")) {
auto parsed = luma::parse_theme(*theme);
if (!parsed) {
return invalid_application(std::string{parsed.error().message()});
}
config.theme = *parsed;
}
if (auto status = luma::validate(config); !status) {
return invalid_application(std::string{status.error().message()});
}
return modulus::success(config);
}
auto luma::load_application_config(std::string const& path) -> std::expected<luma::application_config, modulus::error> {
auto value = modulus::json_config::load_value(path, json_options);
if (!value) {
return modulus::fail<luma::application_config>(value.error());
}
return luma::parse_application_config(*value);
}
auto luma::to_json(luma::display_config const& config) -> boost::json::value {
return boost::json::object{
{
"width",
config.width,
},
{
"height",
config.height,
},
{
"target_fps",
config.target_fps,
},
{
"idle_fps",
config.idle_fps,
},
{
"fullscreen",
config.fullscreen,
},
};
}
auto luma::to_json(luma::touch_config const& config) -> boost::json::value {
return boost::json::object{
{
"max_points",
config.max_points,
},
};
}
auto luma::to_json(luma::application_config const& config) -> boost::json::value {
return boost::json::object{
{
"title",
config.title,
},
{
"display",
luma::to_json(config.display),
},
{
"touch",
luma::to_json(config.touch),
},
{
"theme",
luma::to_json(config.theme),
},
};
}
+77
View File
@@ -0,0 +1,77 @@
#include <luma/control.hpp>
#include <cassert>
#include <cmath>
#include <functional>
#include <utility>
#include <modulus.hpp>
namespace {
auto valid_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;
}
}
luma::control::~control() = default;
auto luma::control::arrange(luma::rect bounds) -> void {
assert(valid_rect(bounds));
bounds_ = bounds;
}
auto luma::control::handle_touch(luma::touch_event const& event) -> std::bool_t {
static_cast<void>(event);
return false;
}
auto luma::control::invalidate_layout() -> void {}
auto luma::control::bind_layout_invalidator(std::function<void()> invalidator) -> void {
layout_invalidator_ = std::move(invalidator);
}
auto luma::control::focusable() const -> std::bool_t {
return false;
}
auto luma::control::set_focused(std::bool_t value) -> void {
assert(!value || focusable());
focused_ = value;
}
auto luma::control::focused() const -> std::bool_t {
return focused_;
}
auto luma::control::set_enabled(std::bool_t value) -> void {
enabled_ = value;
if (!enabled_) {
focused_ = false;
}
}
auto luma::control::enabled() const -> std::bool_t {
return enabled_;
}
auto luma::control::bounds() const -> luma::rect {
assert(valid_rect(bounds_));
return bounds_;
}
auto luma::control::notify_layout_changed() const -> void {
if (!layout_invalidator_) {
return;
}
try {
layout_invalidator_();
} catch (...) {
}
}
auto luma::control::layout_invalidator() const -> std::function<void()> {
return layout_invalidator_;
}
+1631
View File
File diff suppressed because it is too large Load Diff
+10
View File
@@ -0,0 +1,10 @@
#include <luma/detail/errors.hpp>
#include <cstdint>
#include <utility>
#include <modulus.hpp>
auto luma::detail::make_error(std::int32_t code, std::string message, modulus::source_location location) -> modulus::error {
return modulus::make_error("luma", code, std::move(message), location);
}
+359
View File
@@ -0,0 +1,359 @@
#include <luma/overlays.hpp>
#include <cassert>
#include <expected>
#include <exception>
#include <functional>
#include <stdexcept>
#include <string>
#include <utility>
#include <modulus.hpp>
#include <modulus/log.hpp>
#include <luma/detail/errors.hpp>
#include <luma/geometry.hpp>
#include <luma/input.hpp>
#include <luma/render.hpp>
#include <luma/theme.hpp>
namespace {
class scope_exit {
public:
explicit scope_exit(std::function<void()> action);
scope_exit(scope_exit const&) = delete;
auto operator=(scope_exit const&) -> scope_exit& = delete;
~scope_exit();
private:
std::function<void()> action_{};
};
auto invalid(std::string message) -> std::expected<void, modulus::error> {
return luma::detail::fail<void>(luma::detail::error_invalid_argument, std::move(message));
}
auto unavailable(std::string message) -> std::expected<void, modulus::error> {
return luma::detail::fail<void>(luma::detail::error_unavailable, std::move(message));
}
template <typename VALUE>
auto invalid_value(std::string message) -> std::expected<VALUE, modulus::error> {
return luma::detail::fail<VALUE>(luma::detail::error_invalid_argument, std::move(message));
}
auto dialog_bounds(luma::rect bounds) -> luma::rect {
return {
.x = bounds.x + bounds.width * 0.1F,
.y = bounds.y + bounds.height * 0.15F,
.width = bounds.width * 0.8F,
.height = bounds.height * 0.7F,
};
}
auto dialog_body_bounds(luma::rect bounds) -> luma::rect {
return luma::inset(dialog_bounds(bounds), {
16.0F,
56.0F,
16.0F,
72.0F,
});
}
auto accept_bounds(luma::rect bounds, std::bool_t has_cancel) -> luma::rect {
auto dialog = dialog_bounds(bounds);
auto width = 128.0F;
auto x = has_cancel ? luma::right(dialog) - 16.0F - width : dialog.x + (dialog.width - width) * 0.5F;
return {
.x = x,
.y = luma::bottom(dialog) - 56.0F,
.width = width,
.height = 40.0F,
};
}
auto cancel_bounds(luma::rect bounds) -> luma::rect {
auto accept = accept_bounds(bounds, true);
return {
.x = accept.x - 136.0F,
.y = accept.y,
.width = 128.0F,
.height = 40.0F,
};
}
auto draw_button(luma::draw_context& context, luma::theme const& theme, luma::rect bounds, std::string const& text, luma::color fill) -> void {
context.fill_rect(bounds, fill);
context.stroke_rect(bounds, 1.0F, theme.border);
auto measured = context.measure_text(text, theme.body);
context.draw_text(text, {
.x = bounds.x + (bounds.width - measured.width) * 0.5F,
.y = bounds.y + (bounds.height - measured.height) * 0.5F,
}, theme.body, theme.text);
}
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 any_point_inside(luma::touch_event const& event, luma::rect bounds) -> std::bool_t {
for (auto const& point : event.points) {
if (luma::contains(bounds, point.position)) {
return true;
}
}
return false;
}
auto throw_if_failed(std::expected<void, modulus::error> const& status) -> void {
if (!status) {
throw std::runtime_error(status.error().describe());
}
}
scope_exit::scope_exit(std::function<void()> action) : action_{std::move(action)} {}
scope_exit::~scope_exit() {
if (action_) {
action_();
}
}
}
luma::overlay_host::overlay_host(std::unique_ptr<luma::control> content) : content_{std::move(content)} {
assert(content_ != nullptr);
}
auto luma::overlay_host::show_dialog(luma::dialog_options options) -> std::expected<void, modulus::error> {
if (options.body == nullptr) {
return invalid("dialog body must not be null");
}
if (options.accept_text.empty()) {
return invalid("dialog accept text must not be empty");
}
options.body->bind_layout_invalidator(layout_invalidator());
if (dialog_) {
auto body = std::move(dialog_->body);
dialog_.reset();
retire_dialog_body(std::move(body));
}
dialog_ = std::move(options);
modulus::log_debug("luma dialog shown");
return {};
}
auto luma::overlay_host::close_dialog(luma::dialog_result result) -> std::expected<void, modulus::error> {
if (result == luma::dialog_result::NONE) {
return invalid("dialog close result must not be NONE");
}
if (!dialog_) {
return unavailable("no dialog is open");
}
auto callback = std::move(dialog_->on_close);
auto body = std::move(dialog_->body);
dialog_.reset();
retire_dialog_body(std::move(body));
modulus::log_debug("luma dialog closed");
if (callback) {
try {
callback(result);
} catch (std::exception const& error) {
return luma::detail::fail<void>(luma::detail::error_internal, std::string{"dialog close callback failed: "} + error.what());
} catch (...) {
return luma::detail::fail<void>(luma::detail::error_internal, "dialog close callback failed with an unknown exception");
}
}
return {};
}
auto luma::overlay_host::begin_traversal() const -> void {
++traversal_depth_;
}
auto luma::overlay_host::end_traversal() const -> void {
assert(traversal_depth_ > 0);
--traversal_depth_;
flush_retired_dialog_bodies();
}
auto luma::overlay_host::retire_dialog_body(std::unique_ptr<luma::control> body) -> void {
if (body == nullptr) {
return;
}
if (traversal_depth_ == 0) {
body.reset();
return;
}
retired_dialog_bodies_.push_back(std::move(body));
}
auto luma::overlay_host::flush_retired_dialog_bodies() const -> void {
if (traversal_depth_ == 0) {
retired_dialog_bodies_.clear();
}
}
auto luma::overlay_host::show_toast(luma::toast toast) -> std::expected<void, modulus::error> {
if (toast.message.empty()) {
return invalid("toast message must not be empty");
}
if (toast.duration.count() <= 0) {
return invalid("toast duration must be positive");
}
toasts_.push_back(std::move(toast));
return {};
}
auto luma::overlay_host::tick(std::chrono::steady_clock::duration delta) -> std::bool_t {
assert(content_ != nullptr);
assert(delta >= std::chrono::steady_clock::duration::zero());
if (toasts_.empty()) {
toast_elapsed_ = {};
return false;
}
toast_elapsed_ += delta;
if (toast_elapsed_ >= toasts_.front().duration) {
toasts_.pop_front();
toast_elapsed_ = {};
return true;
}
return false;
}
auto luma::overlay_host::measure(luma::draw_context& context, luma::theme const& theme, luma::constraints limits) -> luma::size {
assert(content_ != nullptr);
begin_traversal();
auto guard = scope_exit{[this] {
end_traversal();
}};
auto measured = content_->measure(context, theme, limits);
if (dialog_ && dialog_->body) {
auto* body = dialog_->body.get();
body->measure(context, theme, {
.min = {},
.max = {
.width = limits.max.width * 0.8F,
.height = limits.max.height * 0.8F,
},
});
}
return measured;
}
auto luma::overlay_host::arrange(luma::rect bounds) -> void {
assert(content_ != nullptr);
begin_traversal();
auto guard = scope_exit{[this] {
end_traversal();
}};
luma::control::arrange(bounds);
content_->arrange(bounds);
if (dialog_ && dialog_->body) {
auto* body = dialog_->body.get();
body->arrange(dialog_body_bounds(bounds));
}
}
auto luma::overlay_host::draw(luma::draw_context& context, luma::theme const& theme) const -> void {
assert(content_ != nullptr);
begin_traversal();
auto guard = scope_exit{[this] {
end_traversal();
}};
content_->draw(context, theme);
if (dialog_ && dialog_->body) {
auto dialog = dialog_bounds(bounds());
auto title = dialog_->title;
auto accept_text = dialog_->accept_text;
auto cancel_text = dialog_->cancel_text;
auto* body = dialog_->body.get();
context.fill_rect(dialog, theme.surface);
context.stroke_rect(dialog, 2.0F, theme.accent);
context.draw_text(title, {
.x = dialog.x + 16.0F,
.y = dialog.y + 16.0F,
}, theme.title, theme.text);
body->draw(context, theme);
auto has_cancel = !cancel_text.empty();
if (has_cancel) {
draw_button(context, theme, cancel_bounds(bounds()), cancel_text, theme.surface);
}
draw_button(context, theme, accept_bounds(bounds(), has_cancel), accept_text, theme.surface_active);
}
if (!toasts_.empty()) {
auto toast_bounds = luma::rect{
.x = bounds().x + bounds().width * 0.2F,
.y = bounds().y + bounds().height - 72.0F,
.width = bounds().width * 0.6F,
.height = 48.0F,
};
context.fill_rect(toast_bounds, theme.surface_active);
context.draw_text(toasts_.front().message, {
.x = toast_bounds.x + 16.0F,
.y = toast_bounds.y + 10.0F,
}, theme.body, theme.text);
}
}
auto luma::overlay_host::handle_touch(luma::touch_event const& event) -> std::bool_t {
assert(content_ != nullptr);
begin_traversal();
auto guard = scope_exit{[this] {
end_traversal();
}};
if (dialog_) {
if (event.points.empty()) {
return true;
}
auto has_cancel = !dialog_->cancel_text.empty();
if (first_point_inside(event, accept_bounds(bounds(), has_cancel), luma::touch_phase::UP) != nullptr) {
throw_if_failed(close_dialog(luma::dialog_result::ACCEPTED));
return true;
}
if (has_cancel && first_point_inside(event, cancel_bounds(bounds()), luma::touch_phase::UP) != nullptr) {
throw_if_failed(close_dialog(luma::dialog_result::CANCELLED));
return true;
}
if (dialog_->body && any_point_inside(event, dialog_body_bounds(bounds()))) {
static_cast<void>(dialog_->body->handle_touch(event));
}
return true;
}
return content_->handle_touch(event);
}
auto luma::overlay_host::invalidate_layout() -> void {
assert(content_ != nullptr);
begin_traversal();
auto guard = scope_exit{[this] {
end_traversal();
}};
content_->invalidate_layout();
if (dialog_ && dialog_->body) {
dialog_->body->invalidate_layout();
}
}
auto luma::overlay_host::bind_layout_invalidator(std::function<void()> invalidator) -> void {
luma::control::bind_layout_invalidator(invalidator);
assert(content_ != nullptr);
content_->bind_layout_invalidator(invalidator);
if (dialog_ && dialog_->body) {
dialog_->body->bind_layout_invalidator(std::move(invalidator));
}
}
auto luma::overlays(std::unique_ptr<luma::control> content) -> std::expected<std::unique_ptr<luma::overlay_host>, modulus::error> {
if (content == nullptr) {
return invalid_value<std::unique_ptr<luma::overlay_host>>("overlay content must not be null");
}
return modulus::success(std::unique_ptr<luma::overlay_host>{
new luma::overlay_host{std::move(content)},
});
}
+131
View File
@@ -0,0 +1,131 @@
#include <luma/routing.hpp>
#include <cassert>
#include <expected>
#include <utility>
#include <vector>
#include <modulus.hpp>
#include <modulus/log.hpp>
#include <luma/detail/errors.hpp>
auto luma::router::register_route(std::string name, std::function<std::expected<std::unique_ptr<luma::control>, modulus::error>()> factory) -> std::expected<void, modulus::error> {
if (name.empty()) {
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "route name must not be empty");
}
if (!factory) {
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "route factory must not be empty");
}
routes_[std::move(name)] = std::move(factory);
return {};
}
auto luma::router::set_retire_handler(std::function<void(std::unique_ptr<luma::control>)> handler) -> void {
retire_handler_ = std::move(handler);
}
auto luma::router::replace(std::string const& name) -> std::expected<void, modulus::error> {
if (name.empty()) {
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "route name must not be empty");
}
auto found = routes_.find(name);
if (found == routes_.end()) {
return luma::detail::fail<void>(luma::detail::error_not_found, "route not found: " + name);
}
assert(found->second);
auto view = found->second();
if (!view) {
return modulus::fail<void>(view.error());
}
if (*view == nullptr) {
return luma::detail::fail<void>(luma::detail::error_internal, "route factory returned null view: " + name);
}
retire_stack();
stack_.push_back({
.name = name,
.view = std::move(*view),
});
modulus::log_debug("luma route replaced: " + name);
return {};
}
auto luma::router::push(std::string const& name) -> std::expected<void, modulus::error> {
if (name.empty()) {
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "route name must not be empty");
}
auto found = routes_.find(name);
if (found == routes_.end()) {
return luma::detail::fail<void>(luma::detail::error_not_found, "route not found: " + name);
}
assert(found->second);
auto view = found->second();
if (!view) {
return modulus::fail<void>(view.error());
}
if (*view == nullptr) {
return luma::detail::fail<void>(luma::detail::error_internal, "route factory returned null view: " + name);
}
stack_.push_back({
.name = name,
.view = std::move(*view),
});
modulus::log_debug("luma route pushed: " + name);
return {};
}
auto luma::router::pop() -> std::expected<void, modulus::error> {
if (stack_.size() <= 1) {
return luma::detail::fail<void>(luma::detail::error_unavailable, "cannot pop the last route");
}
auto view = std::move(stack_.back().view);
auto name = stack_.back().name;
stack_.pop_back();
retire(std::move(view));
modulus::log_debug("luma route popped: " + name);
return {};
}
auto luma::router::clear_stack() -> void {
retire_stack();
}
auto luma::router::current() -> luma::control* {
if (stack_.empty()) {
return nullptr;
}
assert(stack_.back().view != nullptr);
return stack_.back().view.get();
}
auto luma::router::current() const -> luma::control const* {
if (stack_.empty()) {
return nullptr;
}
assert(stack_.back().view != nullptr);
return stack_.back().view.get();
}
auto luma::router::current_route() const -> std::string const& {
if (stack_.empty()) {
return empty_route_;
}
return stack_.back().name;
}
auto luma::router::retire(std::unique_ptr<luma::control> view) -> void {
if (view == nullptr) {
return;
}
if (retire_handler_) {
retire_handler_(std::move(view));
}
}
auto luma::router::retire_stack() -> void {
auto removed = std::vector<luma::router::entry>{};
removed.swap(stack_);
for (auto& entry : removed) {
retire(std::move(entry.view));
}
}
+352
View File
@@ -0,0 +1,352 @@
#include <luma/theme.hpp>
#include <cassert>
#include <cmath>
#include <cstdint>
#include <expected>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <boost/json/object.hpp>
#include <boost/json/value.hpp>
#include <modulus.hpp>
#include <luma/detail/errors.hpp>
namespace {
constexpr auto json_options = modulus::json_config::options{
.category = "luma",
.invalid_argument_code = luma::detail::error_invalid_argument,
.not_found_code = luma::detail::error_not_found
};
auto invalid_color(std::string message) -> std::expected<luma::color, modulus::error> {
assert(!message.empty());
return luma::detail::fail<luma::color>(luma::detail::error_invalid_argument, std::move(message));
}
auto invalid_theme(std::string message) -> std::expected<luma::theme, modulus::error> {
assert(!message.empty());
return luma::detail::fail<luma::theme>(luma::detail::error_invalid_argument, std::move(message));
}
auto invalid_status(std::string message) -> std::expected<void, modulus::error> {
assert(!message.empty());
return luma::detail::fail<void>(luma::detail::error_invalid_argument, std::move(message));
}
auto byte_value(boost::json::value const& value, std::char_t const* name) -> std::expected<std::uint8_t, modulus::error> {
assert(name != nullptr);
auto const* integer = value.if_int64();
if (integer == nullptr || *integer < 0 || *integer > 255) {
return luma::detail::fail<std::uint8_t>(luma::detail::error_invalid_argument, std::string{name} + " must be an integer from 0 to 255");
}
return modulus::success(static_cast<std::uint8_t>(*integer));
}
auto apply_color(boost::json::object const& object, std::char_t const* name, luma::color& target) -> std::expected<void, modulus::error> {
assert(name != nullptr);
if (auto const* value = object.if_contains(name)) {
auto parsed = luma::parse_color(*value);
if (!parsed) {
return modulus::fail<void>(parsed.error());
}
target = *parsed;
}
return {};
}
auto number_value(boost::json::value const& value) -> std::optional<std::float64_t>;
auto apply_text_style(boost::json::object const& object, std::char_t const* name, luma::text_style& target) -> std::expected<void, modulus::error> {
assert(name != nullptr);
if (auto const* value = object.if_contains(name)) {
auto style = modulus::json_config::require_object(*value, name, json_options);
if (!style) {
return modulus::fail<void>(style.error());
}
auto unknown = modulus::json_config::reject_unknown_fields(**style, {
"font",
"size",
"line_height",
}, name, json_options);
if (!unknown) {
return modulus::fail<void>(unknown.error());
}
if (auto const* font = (*style)->if_contains("font")) {
auto const* text = font->if_string();
if (text == nullptr) {
return luma::detail::fail<void>(luma::detail::error_invalid_argument, std::string{name} + ".font must be a string");
}
target.font = std::string{text->c_str()};
}
if (auto const* size = (*style)->if_contains("size")) {
auto number = number_value(*size);
if (!number || *number <= 0.0) {
return luma::detail::fail<void>(luma::detail::error_invalid_argument, std::string{name} + ".size must be positive");
}
target.size = static_cast<std::float32_t>(*number);
}
if (auto const* line_height = (*style)->if_contains("line_height")) {
auto number = number_value(*line_height);
if (!number || *number <= 0.0) {
return luma::detail::fail<void>(luma::detail::error_invalid_argument, std::string{name} + ".line_height must be positive");
}
target.line_height = static_cast<std::float32_t>(*number);
}
}
return {};
}
auto number_value(boost::json::value const& value) -> std::optional<std::float64_t> {
if (auto const* number = value.if_double()) {
return *number;
}
if (auto const* integer = value.if_int64()) {
return static_cast<std::float64_t>(*integer);
}
return std::nullopt;
}
}
auto luma::default_theme() -> luma::theme {
return {};
}
auto luma::validate(luma::text_style const& style) -> std::expected<void, modulus::error> {
if (style.font.empty()) {
return invalid_status("text style font must not be empty");
}
if (!std::isfinite(style.size) || style.size <= 0.0F) {
return invalid_status("text style size must be finite and positive");
}
if (!std::isfinite(style.line_height) || style.line_height <= 0.0F) {
return invalid_status("text style line height must be finite and positive");
}
return {};
}
auto luma::validate(luma::theme const& theme) -> std::expected<void, modulus::error> {
if (auto status = luma::validate(theme.body); !status) {
return modulus::fail<void>(status.error());
}
if (auto status = luma::validate(theme.title); !status) {
return modulus::fail<void>(status.error());
}
if (!std::isfinite(theme.radius) || theme.radius < 0.0F) {
return invalid_status("theme.radius must be finite and non-negative");
}
if (!std::isfinite(theme.spacing) || theme.spacing < 0.0F) {
return invalid_status("theme.spacing must be finite and non-negative");
}
return {};
}
auto luma::parse_color(boost::json::value const& value) -> std::expected<luma::color, modulus::error> {
auto object = modulus::json_config::require_object(value, "color", json_options);
if (!object) {
return invalid_color(std::string{object.error().message()});
}
auto unknown = modulus::json_config::reject_unknown_fields(**object, {
"r",
"g",
"b",
"a",
}, "color", json_options);
if (!unknown) {
return invalid_color(std::string{unknown.error().message()});
}
auto const* red_value = (*object)->if_contains("r");
auto const* green_value = (*object)->if_contains("g");
auto const* blue_value = (*object)->if_contains("b");
if (red_value == nullptr || green_value == nullptr || blue_value == nullptr) {
return invalid_color("color requires r, g, and b fields");
}
auto red = byte_value(*red_value, "r");
auto green = byte_value(*green_value, "g");
auto blue = byte_value(*blue_value, "b");
if (!red) {
return invalid_color(std::string{red.error().message()});
}
if (!green) {
return invalid_color(std::string{green.error().message()});
}
if (!blue) {
return invalid_color(std::string{blue.error().message()});
}
auto alpha = std::uint8_t{255};
if (auto const* alpha_value = (*object)->if_contains("a")) {
auto parsed = byte_value(*alpha_value, "a");
if (!parsed) {
return invalid_color(std::string{parsed.error().message()});
}
alpha = *parsed;
}
return modulus::success(luma::color{
.r = *red,
.g = *green,
.b = *blue,
.a = alpha,
});
}
auto luma::parse_theme(boost::json::value const& value) -> std::expected<luma::theme, modulus::error> {
auto object = modulus::json_config::require_object(value, "theme", json_options);
if (!object) {
return invalid_theme(std::string{object.error().message()});
}
auto unknown = modulus::json_config::reject_unknown_fields(**object, {
"background",
"surface",
"surface_active",
"text",
"text_muted",
"border",
"accent",
"body",
"title",
"radius",
"spacing",
}, "theme", json_options);
if (!unknown) {
return invalid_theme(std::string{unknown.error().message()});
}
auto parsed = luma::default_theme();
if (auto status = apply_color(**object, "background", parsed.background); !status) {
return invalid_theme(std::string{status.error().message()});
}
if (auto status = apply_color(**object, "surface", parsed.surface); !status) {
return invalid_theme(std::string{status.error().message()});
}
if (auto status = apply_color(**object, "surface_active", parsed.surface_active); !status) {
return invalid_theme(std::string{status.error().message()});
}
if (auto status = apply_color(**object, "text", parsed.text); !status) {
return invalid_theme(std::string{status.error().message()});
}
if (auto status = apply_color(**object, "text_muted", parsed.text_muted); !status) {
return invalid_theme(std::string{status.error().message()});
}
if (auto status = apply_color(**object, "border", parsed.border); !status) {
return invalid_theme(std::string{status.error().message()});
}
if (auto status = apply_color(**object, "accent", parsed.accent); !status) {
return invalid_theme(std::string{status.error().message()});
}
if (auto status = apply_text_style(**object, "body", parsed.body); !status) {
return invalid_theme(std::string{status.error().message()});
}
if (auto status = apply_text_style(**object, "title", parsed.title); !status) {
return invalid_theme(std::string{status.error().message()});
}
if (auto const* radius = (*object)->if_contains("radius")) {
auto number = number_value(*radius);
if (!number || *number < 0.0) {
return invalid_theme("theme.radius must be non-negative");
}
parsed.radius = static_cast<std::float32_t>(*number);
}
if (auto const* spacing = (*object)->if_contains("spacing")) {
auto number = number_value(*spacing);
if (!number || *number < 0.0) {
return invalid_theme("theme.spacing must be non-negative");
}
parsed.spacing = static_cast<std::float32_t>(*number);
}
if (auto status = luma::validate(parsed); !status) {
return invalid_theme(std::string{status.error().message()});
}
return modulus::success(parsed);
}
auto luma::to_json(luma::color const& color) -> boost::json::value {
return boost::json::object{
{
"r",
static_cast<std::int64_t>(color.r),
},
{
"g",
static_cast<std::int64_t>(color.g),
},
{
"b",
static_cast<std::int64_t>(color.b),
},
{
"a",
static_cast<std::int64_t>(color.a),
},
};
}
auto luma::to_json(luma::text_style const& style) -> boost::json::value {
return boost::json::object{
{
"font",
style.font,
},
{
"size",
static_cast<std::float64_t>(style.size),
},
{
"line_height",
static_cast<std::float64_t>(style.line_height),
},
};
}
auto luma::to_json(luma::theme const& theme) -> boost::json::value {
return boost::json::object{
{
"background",
luma::to_json(theme.background),
},
{
"surface",
luma::to_json(theme.surface),
},
{
"surface_active",
luma::to_json(theme.surface_active),
},
{
"text",
luma::to_json(theme.text),
},
{
"text_muted",
luma::to_json(theme.text_muted),
},
{
"border",
luma::to_json(theme.border),
},
{
"accent",
luma::to_json(theme.accent),
},
{
"body",
luma::to_json(theme.body),
},
{
"title",
luma::to_json(theme.title),
},
{
"radius",
static_cast<std::float64_t>(theme.radius),
},
{
"spacing",
static_cast<std::float64_t>(theme.spacing),
},
};
}
+540
View File
@@ -0,0 +1,540 @@
#include <luma/window.hpp>
#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstdint>
#include <expected>
#include <functional>
#include <string>
#include <utility>
#include <vector>
#include <modulus.hpp>
#include <modulus/log.hpp>
#include <luma/detail/errors.hpp>
#include <luma/geometry.hpp>
#include <luma/input.hpp>
#include <luma/render.hpp>
#include <luma/theme.hpp>
namespace {
class scope_exit {
public:
explicit scope_exit(std::function<void()> action);
scope_exit(scope_exit const&) = delete;
auto operator=(scope_exit const&) -> scope_exit& = delete;
~scope_exit();
private:
std::function<void()> action_{};
};
constexpr auto window_title_vertical_padding = std::float32_t{6.0F};
auto finite_rect(luma::rect value) -> std::bool_t {
return std::isfinite(value.x) && std::isfinite(value.y) && std::isfinite(value.width) && std::isfinite(value.height) && value.width >= 0.0F && value.height >= 0.0F;
}
auto title_height(luma::window_options const& options, std::float32_t line_height) -> std::float32_t {
if (!options.decorated) {
return {};
}
auto desired = line_height + window_title_vertical_padding * 2.0F;
return std::min(desired, options.bounds.height);
}
auto title_bounds(luma::window_options const& options, std::float32_t line_height) -> luma::rect {
return {
.x = options.bounds.x,
.y = options.bounds.y,
.width = options.bounds.width,
.height = title_height(options, line_height),
};
}
auto client_bounds(luma::window_options const& options, std::float32_t line_height) -> luma::rect {
if (!options.decorated) {
return options.bounds;
}
auto title = title_bounds(options, line_height);
return {
.x = options.bounds.x,
.y = options.bounds.y + title.height,
.width = options.bounds.width,
.height = std::max(std::float32_t{}, options.bounds.height - title.height),
};
}
template <typename VALUE>
auto invalid(std::string message) -> std::expected<VALUE, modulus::error> {
return luma::detail::fail<VALUE>(luma::detail::error_invalid_argument, std::move(message));
}
template <typename VALUE>
auto unavailable(std::string message) -> std::expected<VALUE, modulus::error> {
return luma::detail::fail<VALUE>(luma::detail::error_unavailable, std::move(message));
}
auto make_event(luma::touch_event const& event, luma::touch_point point) -> luma::touch_event {
return {
.points = {point},
.time = event.time,
};
}
scope_exit::scope_exit(std::function<void()> action) : action_{std::move(action)} {}
scope_exit::~scope_exit() {
if (action_) {
action_();
}
}
}
luma::window::window(std::uint64_t id, luma::window_options options, std::unique_ptr<luma::control> content) : id_{id}, options_{std::move(options)}, content_{std::move(content)} {
assert(id_ != 0);
assert(finite_rect(options_.bounds));
assert(content_ != nullptr);
}
auto luma::window::id() const -> std::uint64_t {
assert(id_ != 0);
return id_;
}
auto luma::window::options() const -> luma::window_options const& {
assert(finite_rect(options_.bounds));
return options_;
}
auto luma::window::set_bounds(luma::rect bounds) -> std::expected<void, modulus::error> {
if (!finite_rect(bounds)) {
return invalid<void>("window bounds must be finite and non-negative");
}
options_.bounds = bounds;
return {};
}
auto luma::window::set_visible(std::bool_t value) -> void {
options_.visible = value;
}
auto luma::window::visible() const -> std::bool_t {
return options_.visible;
}
auto luma::window::content() -> luma::control* {
assert(content_ != nullptr);
return content_.get();
}
auto luma::window::content() const -> luma::control const* {
assert(content_ != nullptr);
return content_.get();
}
auto luma::window_manager::open(luma::window_options options, std::unique_ptr<luma::control> content) -> std::expected<std::uint64_t, modulus::error> {
if (next_id_ == 0) {
return unavailable<std::uint64_t>("window id space exhausted");
}
if (!finite_rect(options.bounds)) {
return invalid<std::uint64_t>("window bounds must be finite and non-negative");
}
if (content == nullptr) {
return invalid<std::uint64_t>("window content must not be null");
}
content->bind_layout_invalidator(layout_invalidator());
auto id = next_id_++;
auto created = std::unique_ptr<luma::window>{
new luma::window{
id,
std::move(options),
std::move(content),
},
};
if (mutation_depth_ > 0) {
pending_operations_.push_back({
.kind = luma::window_manager::pending_kind::OPEN,
.id = id,
.window = std::move(created),
});
} else {
windows_.push_back(std::move(created));
}
modulus::log_debug("luma window opened: " + std::to_string(id));
return modulus::success(id);
}
auto luma::window_manager::close(std::uint64_t id) -> std::expected<void, modulus::error> {
if (id == 0) {
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "window id must not be zero");
}
if (!contains_effective(id)) {
return luma::detail::fail<void>(luma::detail::error_not_found, "window not found");
}
if (mutation_depth_ > 0) {
pending_operations_.push_back({
.kind = luma::window_manager::pending_kind::CLOSE,
.id = id,
});
} else {
close_now(id);
}
modulus::log_debug("luma window close requested: " + std::to_string(id));
return {};
}
auto luma::window_manager::bring_to_front(std::uint64_t id) -> std::expected<void, modulus::error> {
if (id == 0) {
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "window id must not be zero");
}
if (!contains_effective(id)) {
return luma::detail::fail<void>(luma::detail::error_not_found, "window not found");
}
if (mutation_depth_ > 0) {
pending_operations_.push_back({
.kind = luma::window_manager::pending_kind::BRING_TO_FRONT,
.id = id,
});
} else {
bring_to_front_now(id);
}
return {};
}
auto luma::window_manager::set_bounds(std::uint64_t id, luma::rect bounds) -> std::expected<void, modulus::error> {
if (id == 0) {
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "window id must not be zero");
}
if (!finite_rect(bounds)) {
return invalid<void>("window bounds must be finite and non-negative");
}
if (!contains_effective(id)) {
return luma::detail::fail<void>(luma::detail::error_not_found, "window not found");
}
if (mutation_depth_ > 0) {
pending_operations_.push_back({
.kind = luma::window_manager::pending_kind::SET_BOUNDS,
.id = id,
.bounds = bounds,
});
} else {
set_bounds_now(id, bounds);
}
return {};
}
auto luma::window_manager::set_visible(std::uint64_t id, std::bool_t visible) -> std::expected<void, modulus::error> {
if (id == 0) {
return luma::detail::fail<void>(luma::detail::error_invalid_argument, "window id must not be zero");
}
if (!contains_effective(id)) {
return luma::detail::fail<void>(luma::detail::error_not_found, "window not found");
}
if (mutation_depth_ > 0) {
pending_operations_.push_back({
.kind = luma::window_manager::pending_kind::SET_VISIBLE,
.id = id,
.visible = visible,
});
} else {
set_visible_now(id, visible);
}
return {};
}
auto luma::window_manager::find(std::uint64_t id) -> luma::window* {
if (id == 0) {
return nullptr;
}
auto found = std::find_if(windows_.begin(), windows_.end(), [id](auto const& window) { return window && window->id() == id; });
return found == windows_.end() ? nullptr : found->get();
}
auto luma::window_manager::clear() -> void {
if (mutation_depth_ > 0) {
pending_operations_.push_back({
.kind = luma::window_manager::pending_kind::CLEAR,
});
return;
}
windows_.clear();
active_touches_.clear();
}
auto luma::window_manager::measure(luma::draw_context& context, luma::theme const& theme, luma::constraints limits) -> luma::size {
title_line_height_ = theme.body.line_height;
++mutation_depth_;
auto guard = scope_exit{[this] {
assert(mutation_depth_ > 0);
--mutation_depth_;
apply_pending_operations();
}};
for (auto& window : windows_) {
assert(window != nullptr);
if (!window->visible()) {
continue;
}
assert(window->content() != nullptr);
auto client = client_bounds(window->options(), title_line_height_);
window->content()->measure(context, theme, {
.min = {},
.max = {
.width = client.width,
.height = client.height,
},
});
}
return luma::clamp(limits.max, limits);
}
auto luma::window_manager::arrange(luma::rect bounds) -> void {
assert(finite_rect(bounds));
++mutation_depth_;
auto guard = scope_exit{[this] {
assert(mutation_depth_ > 0);
--mutation_depth_;
apply_pending_operations();
}};
luma::control::arrange(bounds);
for (auto& window : windows_) {
assert(window != nullptr);
if (window->visible()) {
assert(window->content() != nullptr);
window->content()->arrange(client_bounds(window->options(), title_line_height_));
}
}
}
auto luma::window_manager::draw(luma::draw_context& context, luma::theme const& theme) const -> void {
title_line_height_ = theme.body.line_height;
++mutation_depth_;
auto guard = scope_exit{[this] {
assert(mutation_depth_ > 0);
--mutation_depth_;
apply_pending_operations();
}};
for (auto const& window : windows_) {
assert(window != nullptr);
if (!window->visible()) {
continue;
}
auto bounds = window->options().bounds;
auto client = client_bounds(window->options(), title_line_height_);
context.fill_rect(bounds, theme.surface);
assert(window->content() != nullptr);
context.push_clip(client);
window->content()->draw(context, theme);
context.pop_clip();
if (window->options().decorated) {
auto title = title_bounds(window->options(), title_line_height_);
context.fill_rect(title, theme.surface_active);
context.draw_text(window->options().title, {
.x = title.x + 8.0F,
.y = title.y + window_title_vertical_padding,
}, theme.body, theme.text);
}
context.stroke_rect(bounds, 1.0F, theme.border);
}
}
auto luma::window_manager::handle_touch(luma::touch_event const& event) -> std::bool_t {
if (event.points.empty()) {
return false;
}
++mutation_depth_;
auto guard = scope_exit{[this] {
assert(mutation_depth_ > 0);
--mutation_depth_;
apply_pending_operations();
}};
auto handled = false;
for (auto const& point : event.points) {
if (auto captured = active_touches_.find(point.id); captured != active_touches_.end()) {
auto const window_id = captured->second;
auto* captured_window = find(window_id);
auto const captured_visible = captured_window != nullptr && captured_window->visible();
if (captured_visible) {
assert(captured_window->content() != nullptr);
auto single = make_event(event, point);
static_cast<void>(captured_window->content()->handle_touch(single));
handled = true;
}
if (point.phase == luma::touch_phase::UP || point.phase == luma::touch_phase::CANCEL || !captured_visible) {
active_touches_.erase(point.id);
}
continue;
}
for (auto current = windows_.rbegin(); current != windows_.rend(); ++current) {
assert(*current != nullptr);
if (!(*current)->visible()) {
continue;
}
if (luma::contains((*current)->options().bounds, point.position)) {
auto consumed = false;
if (luma::contains(client_bounds((*current)->options(), title_line_height_), point.position)) {
assert((*current)->content() != nullptr);
auto single = make_event(event, point);
consumed = (*current)->content()->handle_touch(single);
if (point.phase == luma::touch_phase::DOWN && (consumed || (*current)->options().modal)) {
active_touches_[point.id] = (*current)->id();
}
}
handled = true;
break;
}
if ((*current)->options().modal) {
handled = true;
break;
}
}
}
return handled;
}
auto luma::window_manager::contains_effective(std::uint64_t id) const -> std::bool_t {
if (id == 0) {
return false;
}
auto exists = std::find_if(windows_.begin(), windows_.end(), [id](auto const& window) { return window && window->id() == id; }) != windows_.end();
for (auto const& operation : pending_operations_) {
switch (operation.kind) {
case luma::window_manager::pending_kind::OPEN:
if (operation.id == id) {
exists = true;
}
break;
case luma::window_manager::pending_kind::CLOSE:
if (operation.id == id) {
exists = false;
}
break;
case luma::window_manager::pending_kind::BRING_TO_FRONT:
case luma::window_manager::pending_kind::SET_BOUNDS:
case luma::window_manager::pending_kind::SET_VISIBLE:
break;
case luma::window_manager::pending_kind::CLEAR:
exists = false;
break;
}
}
return exists;
}
auto luma::window_manager::close_now(std::uint64_t id) const -> void {
auto found = std::find_if(windows_.begin(), windows_.end(), [id](auto const& window) { return window && window->id() == id; });
if (found == windows_.end()) {
return;
}
auto const closed_id = (*found)->id();
for (auto current = active_touches_.begin(); current != active_touches_.end();) {
if (current->second == closed_id) {
current = active_touches_.erase(current);
} else {
++current;
}
}
windows_.erase(found);
}
auto luma::window_manager::bring_to_front_now(std::uint64_t id) const -> void {
auto found = std::find_if(windows_.begin(), windows_.end(), [id](auto const& window) { return window && window->id() == id; });
if (found == windows_.end()) {
return;
}
auto moved = std::move(*found);
windows_.erase(found);
windows_.push_back(std::move(moved));
}
auto luma::window_manager::set_bounds_now(std::uint64_t id, luma::rect bounds) const -> void {
auto* window = const_cast<luma::window_manager*>(this)->find(id);
if (window != nullptr) {
static_cast<void>(window->set_bounds(bounds));
}
}
auto luma::window_manager::set_visible_now(std::uint64_t id, std::bool_t visible) const -> void {
auto* window = const_cast<luma::window_manager*>(this)->find(id);
if (window != nullptr) {
window->set_visible(visible);
}
}
auto luma::window_manager::apply_pending_operations() const -> void {
if (mutation_depth_ != 0 || pending_operations_.empty()) {
return;
}
auto pending = std::vector<luma::window_manager::pending_operation>{};
pending.swap(pending_operations_);
for (auto& operation : pending) {
switch (operation.kind) {
case luma::window_manager::pending_kind::OPEN:
if (operation.window) {
windows_.push_back(std::move(operation.window));
}
break;
case luma::window_manager::pending_kind::CLOSE:
close_now(operation.id);
break;
case luma::window_manager::pending_kind::BRING_TO_FRONT:
bring_to_front_now(operation.id);
break;
case luma::window_manager::pending_kind::SET_BOUNDS:
set_bounds_now(operation.id, operation.bounds);
break;
case luma::window_manager::pending_kind::SET_VISIBLE:
set_visible_now(operation.id, operation.visible);
break;
case luma::window_manager::pending_kind::CLEAR:
windows_.clear();
active_touches_.clear();
break;
}
}
}
auto luma::window_manager::invalidate_layout() -> void {
++mutation_depth_;
auto guard = scope_exit{[this] {
assert(mutation_depth_ > 0);
--mutation_depth_;
apply_pending_operations();
}};
auto controls = std::vector<luma::control*>{};
controls.reserve(windows_.size() + pending_operations_.size());
for (auto& window : windows_) {
assert(window != nullptr);
assert(window->content() != nullptr);
controls.push_back(window->content());
}
for (auto& operation : pending_operations_) {
if (operation.window) {
assert(operation.window->content() != nullptr);
controls.push_back(operation.window->content());
}
}
for (auto* control : controls) {
assert(control != nullptr);
control->invalidate_layout();
}
}
auto luma::window_manager::bind_layout_invalidator(std::function<void()> invalidator) -> void {
luma::control::bind_layout_invalidator(invalidator);
for (auto& window : windows_) {
assert(window != nullptr);
assert(window->content() != nullptr);
window->content()->bind_layout_invalidator(invalidator);
}
for (auto& operation : pending_operations_) {
if (operation.window) {
assert(operation.window->content() != nullptr);
operation.window->content()->bind_layout_invalidator(invalidator);
}
}
}
+28
View File
@@ -0,0 +1,28 @@
add_executable(luma-test
test.cpp
)
find_package(Threads REQUIRED)
target_include_directories(luma-test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(luma-test PRIVATE
luma::luma
Threads::Threads
)
target_compile_features(luma-test PRIVATE cxx_std_23)
target_compile_definitions(luma-test PRIVATE
$<$<CONFIG:Debug>:LUMA_EXPECT_DEBUG_ASSERTS>
$<$<CONFIG:Release>:LUMA_EXPECT_NDEBUG>
$<$<CONFIG:RelWithDebInfo>:LUMA_EXPECT_NDEBUG>
$<$<CONFIG:MinSizeRel>:LUMA_EXPECT_NDEBUG>
)
add_test(NAME luma COMMAND luma-test)
set_tests_properties(luma PROPERTIES TIMEOUT 20)
add_custom_target(check
COMMAND ${CMAKE_CTEST_COMMAND}
--test-dir ${CMAKE_BINARY_DIR}
-R ^luma$
--output-on-failure
--timeout 20
USES_TERMINAL
)
+2387
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
#pragma once
#define BOOST_TEST_MODULE luma
#include <boost/test/included/unit_test.hpp>