luvatrix 0.2.0__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {luvatrix-0.2.0/luvatrix.egg-info → luvatrix-0.2.2}/PKG-INFO +43 -1
- {luvatrix-0.2.0 → luvatrix-0.2.2}/README.md +42 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix/app.py +64 -23
- {luvatrix-0.2.0 → luvatrix-0.2.2/luvatrix.egg-info}/PKG-INFO +43 -1
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix.egg-info/SOURCES.txt +3 -1
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/__init__.py +1 -1
- luvatrix-0.2.2/luvatrix_core/core/interaction_work.py +162 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/android/runner.py +32 -4
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/text/__init__.py +7 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/text/component.py +72 -0
- luvatrix-0.2.2/luvatrix_ui/text/wrapping.py +233 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/pyproject.toml +1 -1
- {luvatrix-0.2.0 → luvatrix-0.2.2}/LICENSE +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/MANIFEST.in +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix/__init__.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix/auth/__init__.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix/auth/calendar.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix/auth/google.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix/auth/sign_in.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix/auth/ui.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix.egg-info/dependency_links.txt +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix.egg-info/entry_points.txt +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix.egg-info/requires.txt +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix.egg-info/top_level.txt +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/accel.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/__init__.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/app_runtime.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/audit.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/coordinates.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/debug_capture.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/debug_menu.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/display_runtime.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/energy_safety.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/engine.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/events.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/frame_rate_controller.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/hdi_thread.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/process_runtime.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/process_sdk.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/protocol_governance.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/scene_display_runtime.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/scene_graph.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/scene_rasterizer.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/sensor_manager.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/ui_frame_renderer.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/unified_runtime.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/window_matrix.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/perf/copy_telemetry.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/__init__.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/android/__init__.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/android/camera_processing_contract.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/android/camera_style.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/android/hdi_source.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/android/scene_target.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/android/sensors.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/android/vulkan_target.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/frame_pipeline.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/ios/__init__.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/ios/hdi_source.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/ios/lifecycle.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/ios/metal_backend.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/ios/runner.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/ios/scene_target.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/ios/window_system.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/macos/__init__.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/macos/hdi_source.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/macos/iohid_source.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/macos/metal_backend.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/macos/metal_presenter.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/macos/metal_scene_target.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/macos/sensors.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/macos/vulkan_backend.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/macos/vulkan_presenter.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/macos/window_system.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/package_sync.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/vulkan_compat.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/vulkan_scaling.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/vulkan_setup.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/web/__init__.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/web/build.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/web/command_buffer.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/web/python_shim/luvatrix/__init__.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/web/python_shim/luvatrix/app.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/web/runtime_assets/index.html +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/web/runtime_assets/runtime/command-buffer.js +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/web/runtime_assets/runtime/input.js +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/web/runtime_assets/runtime/luvatrix-web.js +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/web/runtime_assets/runtime/package.json +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/web/runtime_assets/runtime/renderers.js +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/web/runtime_assets/runtime/spotify-bridge.js +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/web/server.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/web/websocket_target.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/render/__init__.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/render/framebuffer.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/render/svg.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/scaffold.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/targets/__init__.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/targets/base.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/targets/cpu_scene_target.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/targets/metal_target.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/targets/scene_target.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/targets/vulkan_target.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/targets/web_target.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/.gitignore +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/build.gradle.kts +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/AndroidManifest.xml +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/assets/luvatrix_bitmap_font.txt +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/assets/luvatrix_launch_config.json +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/assets/luvatrix_matrix_font_alpha.txt +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/cpp/CMakeLists.txt +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/cpp/luvatrix_camera_preview.frag +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/cpp/luvatrix_camera_preview_shaders.h +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/cpp/luvatrix_vulkan_renderer.cpp +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/java/com/luvatrix/app/CameraBridge.kt +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/java/com/luvatrix/app/LuvatrixVulkanView.kt +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/java/com/luvatrix/app/MainActivity.kt +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/java/com/luvatrix/app/NativeVulkan.kt +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/python/.gitignore +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/python/luvatrix_android_boot.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/python/luvatrix_launch_config.json +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/res/values/styles.xml +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/build.gradle.kts +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/gradle.properties +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/gradlew +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/scripts/emulator_acceptance.sh +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/scripts/sync_python_assets.sh +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/settings.gradle.kts +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/ios/Luvatrix/AppDelegate.swift +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/ios/Luvatrix/Assets.xcassets/AppIcon.appiconset/Contents.json +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/ios/Luvatrix/Assets.xcassets/AppIcon.appiconset/icon.png +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/ios/Luvatrix/Assets.xcassets/Contents.json +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/ios/Luvatrix/Info.plist +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/ios/Luvatrix/Luvatrix-Bridging-Header.h +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/ios/Luvatrix/Luvatrix.entitlements +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/ios/Luvatrix/PythonBridge.h +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/ios/Luvatrix/PythonBridge.m +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/ios/Luvatrix/main.swift +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/ios/project.yml +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/ios/scripts/setup_ios.sh +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/ui/__init__.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/ui/element.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/ui/page_loader.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/__init__.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/adapters/__init__.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/adapters/normalize.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/api.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/compile/__init__.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/compile/app_protocol.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/display.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/dynamic_axis.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/errors.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/figure.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/live.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/raster/__init__.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/raster/canvas.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/raster/draw_lines.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/raster/draw_markers.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/raster/draw_text.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/raster/layers.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/scales.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/series.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/__init__.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/component_schema.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/controls/__init__.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/controls/button.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/controls/interaction.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/controls/stained_glass_button.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/controls/svg_component.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/controls/svg_renderer.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/planes_protocol.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/planes_runtime.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/planes_v2_validator.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/planning/__init__.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/planning/agile_renderer.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/planning/exporters.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/planning/gantt_renderer.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/planning/interaction.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/planning/schema.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/planning/validation.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/style/__init__.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/style/theme.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/table/__init__.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/table/component.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/text/renderer.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/ui_ir.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/main.py +0 -0
- {luvatrix-0.2.0 → luvatrix-0.2.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: luvatrix
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Luvatrix runtime and plotting toolkit
|
|
5
5
|
Author: Luvatrix contributors
|
|
6
6
|
License-Expression: LicenseRef-Proprietary
|
|
@@ -215,3 +215,45 @@ Android Keystore. iOS bridges should use `ASWebAuthenticationSession` and
|
|
|
215
215
|
Keychain. `InMemoryTokenStore` remains available for tests, but production apps
|
|
216
216
|
should not use it for long-lived refresh tokens. Mobile apps must not embed a
|
|
217
217
|
Google client secret.
|
|
218
|
+
|
|
219
|
+
## Prepared Text Wrapping
|
|
220
|
+
|
|
221
|
+
Luvatrix includes an optional, dependency-free prepared wrapping style inspired
|
|
222
|
+
by the MIT-licensed [Pretext](https://github.com/chenglou/pretext) architecture.
|
|
223
|
+
Text is segmented and measured once; changing only the available width reuses
|
|
224
|
+
those native measurements.
|
|
225
|
+
|
|
226
|
+
Scene/UI text uses `TextWrapping` on `TextComponent`:
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
from luvatrix.app import TextWrapping
|
|
230
|
+
from luvatrix_ui.text import TextComponent
|
|
231
|
+
|
|
232
|
+
body = TextComponent(
|
|
233
|
+
component_id="body",
|
|
234
|
+
text="A paragraph which should wrap across multiple lines.",
|
|
235
|
+
max_width_px=320,
|
|
236
|
+
wrapping=TextWrapping(white_space="normal"),
|
|
237
|
+
)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Matrix text uses the same layout engine and its matrix-font measurements:
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
from luvatrix.app import draw_text_to_matrix
|
|
244
|
+
|
|
245
|
+
draw_text_to_matrix(
|
|
246
|
+
matrix,
|
|
247
|
+
"A paragraph rendered into an RGBA matrix.",
|
|
248
|
+
x=12,
|
|
249
|
+
y=12,
|
|
250
|
+
font_size_px=12,
|
|
251
|
+
max_width_px=240,
|
|
252
|
+
wrapping="pretext",
|
|
253
|
+
line_height_multiplier=1.2,
|
|
254
|
+
)
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
`white_space="pre-wrap"` preserves repeated spaces and hard line breaks.
|
|
258
|
+
Ordinary wrapping collapses whitespace and uses grapheme-safe emergency breaks
|
|
259
|
+
for content wider than a complete line.
|
|
@@ -177,3 +177,45 @@ Android Keystore. iOS bridges should use `ASWebAuthenticationSession` and
|
|
|
177
177
|
Keychain. `InMemoryTokenStore` remains available for tests, but production apps
|
|
178
178
|
should not use it for long-lived refresh tokens. Mobile apps must not embed a
|
|
179
179
|
Google client secret.
|
|
180
|
+
|
|
181
|
+
## Prepared Text Wrapping
|
|
182
|
+
|
|
183
|
+
Luvatrix includes an optional, dependency-free prepared wrapping style inspired
|
|
184
|
+
by the MIT-licensed [Pretext](https://github.com/chenglou/pretext) architecture.
|
|
185
|
+
Text is segmented and measured once; changing only the available width reuses
|
|
186
|
+
those native measurements.
|
|
187
|
+
|
|
188
|
+
Scene/UI text uses `TextWrapping` on `TextComponent`:
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
from luvatrix.app import TextWrapping
|
|
192
|
+
from luvatrix_ui.text import TextComponent
|
|
193
|
+
|
|
194
|
+
body = TextComponent(
|
|
195
|
+
component_id="body",
|
|
196
|
+
text="A paragraph which should wrap across multiple lines.",
|
|
197
|
+
max_width_px=320,
|
|
198
|
+
wrapping=TextWrapping(white_space="normal"),
|
|
199
|
+
)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Matrix text uses the same layout engine and its matrix-font measurements:
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
from luvatrix.app import draw_text_to_matrix
|
|
206
|
+
|
|
207
|
+
draw_text_to_matrix(
|
|
208
|
+
matrix,
|
|
209
|
+
"A paragraph rendered into an RGBA matrix.",
|
|
210
|
+
x=12,
|
|
211
|
+
y=12,
|
|
212
|
+
font_size_px=12,
|
|
213
|
+
max_width_px=240,
|
|
214
|
+
wrapping="pretext",
|
|
215
|
+
line_height_multiplier=1.2,
|
|
216
|
+
)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`white_space="pre-wrap"` preserves repeated spaces and hard line breaks.
|
|
220
|
+
Ordinary wrapping collapses whitespace and uses grapheme-safe emergency breaks
|
|
221
|
+
for content wider than a complete line.
|
|
@@ -22,9 +22,11 @@ from luvatrix_core.core.app_runtime import (
|
|
|
22
22
|
ResolvedAppVariant,
|
|
23
23
|
)
|
|
24
24
|
from luvatrix_core.core.hdi_thread import HDIEvent, HDIThread
|
|
25
|
+
from luvatrix_core.core.interaction_work import InteractionAwareWorkScheduler
|
|
25
26
|
from luvatrix_core.core.sensor_manager import SensorManagerThread, SensorSample
|
|
26
27
|
from luvatrix_core.core.window_matrix import FullRewrite, WriteBatch, WindowMatrix
|
|
27
28
|
from luvatrix_core import accel
|
|
29
|
+
from luvatrix_ui.text.wrapping import TextWrapping, layout_text, prepare_text
|
|
28
30
|
|
|
29
31
|
PLATFORM_MACOS = "macos"
|
|
30
32
|
PLATFORM_IOS = "ios"
|
|
@@ -298,6 +300,11 @@ class SwipeMomentumController:
|
|
|
298
300
|
def dragging(self) -> bool:
|
|
299
301
|
return self._dragging
|
|
300
302
|
|
|
303
|
+
@property
|
|
304
|
+
def active(self) -> bool:
|
|
305
|
+
"""Whether a drag or post-release momentum is active."""
|
|
306
|
+
return self._dragging or self._velocity != 0.0
|
|
307
|
+
|
|
301
308
|
def reset(self) -> None:
|
|
302
309
|
self._touch_id = None
|
|
303
310
|
self._last_position = None
|
|
@@ -1262,11 +1269,15 @@ def draw_text_to_matrix(
|
|
|
1262
1269
|
font_size_px: float = 14.0,
|
|
1263
1270
|
color: tuple[int, int, int, int] | str = (255, 255, 255, 255),
|
|
1264
1271
|
antialias: bool = True,
|
|
1272
|
+
max_width_px: float | None = None,
|
|
1273
|
+
wrapping: str | TextWrapping | None = None,
|
|
1274
|
+
line_height_multiplier: float = 1.2,
|
|
1265
1275
|
):
|
|
1266
1276
|
"""Rasterize Luvatrix's default matrix text glyphs into an RGBA matrix.
|
|
1267
1277
|
|
|
1268
1278
|
Set ``antialias=False`` for crisp, thresholded glyph pixels on compact
|
|
1269
|
-
matrix surfaces such as calendar event tiles.
|
|
1279
|
+
matrix surfaces such as calendar event tiles. Set ``wrapping="pretext"``
|
|
1280
|
+
with ``max_width_px`` for prepared multiline wrapping.
|
|
1270
1281
|
"""
|
|
1271
1282
|
height = int(matrix.shape[0])
|
|
1272
1283
|
width = int(matrix.shape[1])
|
|
@@ -1275,30 +1286,58 @@ def draw_text_to_matrix(
|
|
|
1275
1286
|
|
|
1276
1287
|
font = _default_matrix_font()
|
|
1277
1288
|
scale = max(1, int(round(float(font_size_px) / float(font.height))))
|
|
1278
|
-
|
|
1279
|
-
|
|
1289
|
+
line_height = max(1.0, float(font_size_px) * float(line_height_multiplier))
|
|
1290
|
+
if line_height_multiplier <= 0:
|
|
1291
|
+
raise ValueError("line_height_multiplier must be > 0")
|
|
1292
|
+
if wrapping is not None and max_width_px is None:
|
|
1293
|
+
raise ValueError("max_width_px is required when matrix text wrapping is enabled")
|
|
1294
|
+
if isinstance(wrapping, str):
|
|
1295
|
+
if wrapping != "pretext":
|
|
1296
|
+
raise ValueError("wrapping must be None, 'pretext', or TextWrapping")
|
|
1297
|
+
wrapping = TextWrapping()
|
|
1298
|
+
if wrapping is not None and not isinstance(wrapping, TextWrapping):
|
|
1299
|
+
raise TypeError("wrapping must be None, 'pretext', or TextWrapping")
|
|
1300
|
+
|
|
1301
|
+
lines = (str(text),)
|
|
1302
|
+
if wrapping is not None:
|
|
1303
|
+
advance = font.advance * scale
|
|
1304
|
+
prepared = prepare_text(
|
|
1305
|
+
str(text),
|
|
1306
|
+
measure=lambda value: float(len(value) * advance),
|
|
1307
|
+
wrapping=wrapping,
|
|
1308
|
+
)
|
|
1309
|
+
wrapped = layout_text(
|
|
1310
|
+
prepared,
|
|
1311
|
+
max_width_px=float(max_width_px),
|
|
1312
|
+
line_height_px=line_height,
|
|
1313
|
+
)
|
|
1314
|
+
lines = tuple(line.text for line in wrapped.lines)
|
|
1315
|
+
|
|
1280
1316
|
rgba = _rgba(color)
|
|
1281
1317
|
space = font.glyphs[" "]
|
|
1282
|
-
for
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1318
|
+
for line_index, line in enumerate(lines):
|
|
1319
|
+
cursor = int(x)
|
|
1320
|
+
top = int(float(y) + line_index * line_height)
|
|
1321
|
+
for raw_ch in line:
|
|
1322
|
+
glyph = font.glyphs.get(raw_ch, font.glyphs.get(raw_ch.upper(), space))
|
|
1323
|
+
for gy, row in enumerate(glyph):
|
|
1324
|
+
for gx, coverage in enumerate(row):
|
|
1325
|
+
if not antialias:
|
|
1326
|
+
coverage = 255 if coverage >= 128 else 0
|
|
1327
|
+
if coverage <= 0:
|
|
1328
|
+
continue
|
|
1329
|
+
_paint_matrix_rect(
|
|
1330
|
+
matrix,
|
|
1331
|
+
cursor + gx * scale,
|
|
1332
|
+
top + gy * scale,
|
|
1333
|
+
cursor + (gx + 1) * scale,
|
|
1334
|
+
top + (gy + 1) * scale,
|
|
1335
|
+
rgba,
|
|
1336
|
+
coverage=coverage,
|
|
1337
|
+
width=width,
|
|
1338
|
+
height=height,
|
|
1339
|
+
)
|
|
1340
|
+
cursor += font.advance * scale
|
|
1302
1341
|
return matrix
|
|
1303
1342
|
|
|
1304
1343
|
|
|
@@ -1823,6 +1862,7 @@ __all__ = [
|
|
|
1823
1862
|
"Display",
|
|
1824
1863
|
"InputManager",
|
|
1825
1864
|
"InputState",
|
|
1865
|
+
"InteractionAwareWorkScheduler",
|
|
1826
1866
|
"MatrixFrame",
|
|
1827
1867
|
"MissingOptionalDependencyError",
|
|
1828
1868
|
"ResolvedAppVariant",
|
|
@@ -1833,6 +1873,7 @@ __all__ = [
|
|
|
1833
1873
|
"Sensors",
|
|
1834
1874
|
"SwipeMomentumController",
|
|
1835
1875
|
"SwipeMomentumUpdate",
|
|
1876
|
+
"TextWrapping",
|
|
1836
1877
|
"UIFrame",
|
|
1837
1878
|
"apply_hdi_events",
|
|
1838
1879
|
"check_app_install",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: luvatrix
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Luvatrix runtime and plotting toolkit
|
|
5
5
|
Author: Luvatrix contributors
|
|
6
6
|
License-Expression: LicenseRef-Proprietary
|
|
@@ -215,3 +215,45 @@ Android Keystore. iOS bridges should use `ASWebAuthenticationSession` and
|
|
|
215
215
|
Keychain. `InMemoryTokenStore` remains available for tests, but production apps
|
|
216
216
|
should not use it for long-lived refresh tokens. Mobile apps must not embed a
|
|
217
217
|
Google client secret.
|
|
218
|
+
|
|
219
|
+
## Prepared Text Wrapping
|
|
220
|
+
|
|
221
|
+
Luvatrix includes an optional, dependency-free prepared wrapping style inspired
|
|
222
|
+
by the MIT-licensed [Pretext](https://github.com/chenglou/pretext) architecture.
|
|
223
|
+
Text is segmented and measured once; changing only the available width reuses
|
|
224
|
+
those native measurements.
|
|
225
|
+
|
|
226
|
+
Scene/UI text uses `TextWrapping` on `TextComponent`:
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
from luvatrix.app import TextWrapping
|
|
230
|
+
from luvatrix_ui.text import TextComponent
|
|
231
|
+
|
|
232
|
+
body = TextComponent(
|
|
233
|
+
component_id="body",
|
|
234
|
+
text="A paragraph which should wrap across multiple lines.",
|
|
235
|
+
max_width_px=320,
|
|
236
|
+
wrapping=TextWrapping(white_space="normal"),
|
|
237
|
+
)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Matrix text uses the same layout engine and its matrix-font measurements:
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
from luvatrix.app import draw_text_to_matrix
|
|
244
|
+
|
|
245
|
+
draw_text_to_matrix(
|
|
246
|
+
matrix,
|
|
247
|
+
"A paragraph rendered into an RGBA matrix.",
|
|
248
|
+
x=12,
|
|
249
|
+
y=12,
|
|
250
|
+
font_size_px=12,
|
|
251
|
+
max_width_px=240,
|
|
252
|
+
wrapping="pretext",
|
|
253
|
+
line_height_multiplier=1.2,
|
|
254
|
+
)
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
`white_space="pre-wrap"` preserves repeated spaces and hard line breaks.
|
|
258
|
+
Ordinary wrapping collapses whitespace and uses grapheme-safe emergency breaks
|
|
259
|
+
for content wider than a complete line.
|
|
@@ -31,6 +31,7 @@ luvatrix_core/core/engine.py
|
|
|
31
31
|
luvatrix_core/core/events.py
|
|
32
32
|
luvatrix_core/core/frame_rate_controller.py
|
|
33
33
|
luvatrix_core/core/hdi_thread.py
|
|
34
|
+
luvatrix_core/core/interaction_work.py
|
|
34
35
|
luvatrix_core/core/process_runtime.py
|
|
35
36
|
luvatrix_core/core/process_sdk.py
|
|
36
37
|
luvatrix_core/core/protocol_governance.py
|
|
@@ -185,4 +186,5 @@ luvatrix_ui/table/__init__.py
|
|
|
185
186
|
luvatrix_ui/table/component.py
|
|
186
187
|
luvatrix_ui/text/__init__.py
|
|
187
188
|
luvatrix_ui/text/component.py
|
|
188
|
-
luvatrix_ui/text/renderer.py
|
|
189
|
+
luvatrix_ui/text/renderer.py
|
|
190
|
+
luvatrix_ui/text/wrapping.py
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import deque
|
|
4
|
+
from collections.abc import Callable, Hashable
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class _WorkItem:
|
|
13
|
+
key: Hashable
|
|
14
|
+
prepare: Callable[[], Any]
|
|
15
|
+
on_complete: Callable[[Any], None] | None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class InteractionAwareWorkScheduler:
|
|
19
|
+
"""Run keyed preparation work only while user interaction is idle.
|
|
20
|
+
|
|
21
|
+
Work and callbacks execute sequentially on one daemon thread. Running work
|
|
22
|
+
is not preempted when interaction begins; the idle gate prevents queued
|
|
23
|
+
work from starting until ``interaction_active`` returns false.
|
|
24
|
+
|
|
25
|
+
Pair with ``SwipeMomentumController`` using
|
|
26
|
+
``interaction_active=lambda: swipe.active`` and call
|
|
27
|
+
:meth:`notify_interaction_state_changed` after controller updates to wake
|
|
28
|
+
queued work immediately when momentum stops.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
*,
|
|
34
|
+
interaction_active: Callable[[], bool],
|
|
35
|
+
request_render: Callable[[], None] | None = None,
|
|
36
|
+
on_error: Callable[[Hashable, Exception], None] | None = None,
|
|
37
|
+
idle_poll_interval: float = 1.0 / 120.0,
|
|
38
|
+
thread_name: str = "luvatrix-interaction-work",
|
|
39
|
+
) -> None:
|
|
40
|
+
self.interaction_active = interaction_active
|
|
41
|
+
self.request_render = request_render
|
|
42
|
+
self.on_error = on_error
|
|
43
|
+
self.idle_poll_interval = max(0.001, float(idle_poll_interval))
|
|
44
|
+
self.thread_name = str(thread_name)
|
|
45
|
+
self._condition = threading.Condition()
|
|
46
|
+
self._pending: deque[_WorkItem] = deque()
|
|
47
|
+
self._keys: set[Hashable] = set()
|
|
48
|
+
self._thread: threading.Thread | None = None
|
|
49
|
+
self._closed = False
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def pending_count(self) -> int:
|
|
53
|
+
with self._condition:
|
|
54
|
+
return len(self._keys)
|
|
55
|
+
|
|
56
|
+
def submit(
|
|
57
|
+
self,
|
|
58
|
+
key: Hashable,
|
|
59
|
+
prepare: Callable[[], Any],
|
|
60
|
+
*,
|
|
61
|
+
on_complete: Callable[[Any], None] | None = None,
|
|
62
|
+
) -> bool:
|
|
63
|
+
"""Queue work unless the key is already queued or running."""
|
|
64
|
+
with self._condition:
|
|
65
|
+
if self._closed:
|
|
66
|
+
raise RuntimeError("interaction-aware work scheduler is closed")
|
|
67
|
+
if key in self._keys:
|
|
68
|
+
return False
|
|
69
|
+
self._keys.add(key)
|
|
70
|
+
self._pending.append(_WorkItem(key, prepare, on_complete))
|
|
71
|
+
if self._thread is None:
|
|
72
|
+
self._thread = threading.Thread(
|
|
73
|
+
target=self._run,
|
|
74
|
+
name=self.thread_name,
|
|
75
|
+
daemon=True,
|
|
76
|
+
)
|
|
77
|
+
self._thread.start()
|
|
78
|
+
self._condition.notify_all()
|
|
79
|
+
return True
|
|
80
|
+
|
|
81
|
+
def notify_interaction_state_changed(self) -> None:
|
|
82
|
+
"""Wake idle-gated work after a controller state update."""
|
|
83
|
+
with self._condition:
|
|
84
|
+
self._condition.notify_all()
|
|
85
|
+
|
|
86
|
+
def wait_idle(self, timeout: float | None = None) -> bool:
|
|
87
|
+
"""Wait until queued/running work and its callbacks have completed."""
|
|
88
|
+
deadline = None if timeout is None else time.monotonic() + max(0.0, float(timeout))
|
|
89
|
+
with self._condition:
|
|
90
|
+
while self._keys:
|
|
91
|
+
if deadline is None:
|
|
92
|
+
self._condition.wait()
|
|
93
|
+
continue
|
|
94
|
+
remaining = deadline - time.monotonic()
|
|
95
|
+
if remaining <= 0.0:
|
|
96
|
+
return False
|
|
97
|
+
self._condition.wait(remaining)
|
|
98
|
+
return True
|
|
99
|
+
|
|
100
|
+
def close(self, *, wait: bool = False) -> None:
|
|
101
|
+
"""Cancel queued work and optionally wait for running work to finish."""
|
|
102
|
+
with self._condition:
|
|
103
|
+
self._closed = True
|
|
104
|
+
while self._pending:
|
|
105
|
+
self._keys.discard(self._pending.popleft().key)
|
|
106
|
+
thread = self._thread
|
|
107
|
+
self._condition.notify_all()
|
|
108
|
+
if wait and thread is not None and thread is not threading.current_thread():
|
|
109
|
+
thread.join()
|
|
110
|
+
|
|
111
|
+
def _run(self) -> None:
|
|
112
|
+
while True:
|
|
113
|
+
with self._condition:
|
|
114
|
+
while not self._closed and not self._pending:
|
|
115
|
+
self._condition.wait()
|
|
116
|
+
if self._closed:
|
|
117
|
+
return
|
|
118
|
+
pending_key = self._pending[0].key
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
interaction_active = bool(self.interaction_active())
|
|
122
|
+
except Exception as exc:
|
|
123
|
+
self._report_error(pending_key, exc)
|
|
124
|
+
interaction_active = False
|
|
125
|
+
|
|
126
|
+
with self._condition:
|
|
127
|
+
if self._closed:
|
|
128
|
+
return
|
|
129
|
+
if not self._pending:
|
|
130
|
+
continue
|
|
131
|
+
if interaction_active:
|
|
132
|
+
self._condition.wait(self.idle_poll_interval)
|
|
133
|
+
continue
|
|
134
|
+
item = self._pending.popleft()
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
value = item.prepare()
|
|
138
|
+
except Exception as exc:
|
|
139
|
+
self._report_error(item.key, exc)
|
|
140
|
+
else:
|
|
141
|
+
if item.on_complete is not None:
|
|
142
|
+
try:
|
|
143
|
+
item.on_complete(value)
|
|
144
|
+
except Exception as exc:
|
|
145
|
+
self._report_error(item.key, exc)
|
|
146
|
+
try:
|
|
147
|
+
if self.request_render is not None:
|
|
148
|
+
self.request_render()
|
|
149
|
+
except Exception as exc:
|
|
150
|
+
self._report_error(item.key, exc)
|
|
151
|
+
finally:
|
|
152
|
+
with self._condition:
|
|
153
|
+
self._keys.discard(item.key)
|
|
154
|
+
self._condition.notify_all()
|
|
155
|
+
|
|
156
|
+
def _report_error(self, key: Hashable, error: Exception) -> None:
|
|
157
|
+
if self.on_error is None:
|
|
158
|
+
return
|
|
159
|
+
try:
|
|
160
|
+
self.on_error(key, error)
|
|
161
|
+
except Exception:
|
|
162
|
+
return
|
|
@@ -332,14 +332,17 @@ def sync_android_python_assets(app_dir: Path, *, project_dir: Path) -> None:
|
|
|
332
332
|
|
|
333
333
|
def _android_python_packages_for_app(app_dir: Path, *, exclude_dirs: Iterable[Path] = ()) -> tuple[str, ...]:
|
|
334
334
|
packages = ["luvatrix", "luvatrix_core"]
|
|
335
|
-
|
|
335
|
+
imported_modules = _imported_modules_for_app(app_dir, exclude_dirs=exclude_dirs)
|
|
336
|
+
imports = {module.split(".", 1)[0] for module in imported_modules}
|
|
337
|
+
for module in imported_modules:
|
|
338
|
+
imports.update(_first_party_module_import_roots(module))
|
|
336
339
|
for optional_pkg in ("luvatrix_ui", "luvatrix_plot"):
|
|
337
340
|
if optional_pkg in imports:
|
|
338
341
|
packages.append(optional_pkg)
|
|
339
342
|
return tuple(packages)
|
|
340
343
|
|
|
341
344
|
|
|
342
|
-
def
|
|
345
|
+
def _imported_modules_for_app(app_dir: Path, *, exclude_dirs: Iterable[Path] = ()) -> set[str]:
|
|
343
346
|
imports: set[str] = set()
|
|
344
347
|
excluded = tuple(path.resolve() for path in exclude_dirs)
|
|
345
348
|
for path in app_dir.rglob("*.py"):
|
|
@@ -355,12 +358,37 @@ def _top_level_imports_for_app(app_dir: Path, *, exclude_dirs: Iterable[Path] =
|
|
|
355
358
|
for node in ast.walk(tree):
|
|
356
359
|
if isinstance(node, ast.Import):
|
|
357
360
|
for alias in node.names:
|
|
358
|
-
imports.add(alias.name
|
|
361
|
+
imports.add(alias.name)
|
|
359
362
|
elif isinstance(node, ast.ImportFrom) and node.module:
|
|
360
|
-
imports.add(node.module
|
|
363
|
+
imports.add(node.module)
|
|
361
364
|
return imports
|
|
362
365
|
|
|
363
366
|
|
|
367
|
+
def _first_party_module_import_roots(module_name: str) -> set[str]:
|
|
368
|
+
package_name, _, relative_module = module_name.partition(".")
|
|
369
|
+
if package_name not in {"luvatrix", "luvatrix_core"}:
|
|
370
|
+
return set()
|
|
371
|
+
package_dir = _python_package_dir(package_name)
|
|
372
|
+
if relative_module:
|
|
373
|
+
module_path = package_dir.joinpath(*relative_module.split("."))
|
|
374
|
+
source_path = module_path.with_suffix(".py")
|
|
375
|
+
if not source_path.is_file():
|
|
376
|
+
source_path = module_path / "__init__.py"
|
|
377
|
+
else:
|
|
378
|
+
source_path = package_dir / "__init__.py"
|
|
379
|
+
try:
|
|
380
|
+
tree = ast.parse(source_path.read_text(encoding="utf-8"), filename=str(source_path))
|
|
381
|
+
except (OSError, SyntaxError):
|
|
382
|
+
return set()
|
|
383
|
+
roots: set[str] = set()
|
|
384
|
+
for node in ast.walk(tree):
|
|
385
|
+
if isinstance(node, ast.Import):
|
|
386
|
+
roots.update(alias.name.split(".", 1)[0] for alias in node.names)
|
|
387
|
+
elif isinstance(node, ast.ImportFrom) and node.module:
|
|
388
|
+
roots.add(node.module.split(".", 1)[0])
|
|
389
|
+
return roots
|
|
390
|
+
|
|
391
|
+
|
|
364
392
|
def _make_android_app_ignore(project_dir: Path):
|
|
365
393
|
pattern_ignore = shutil.ignore_patterns("__pycache__", "*.pyc", ".pytest_cache", ".luvatrix")
|
|
366
394
|
project = project_dir.resolve()
|
|
@@ -11,6 +11,7 @@ from .renderer import (
|
|
|
11
11
|
TextRenderer,
|
|
12
12
|
TextSizeSpec,
|
|
13
13
|
)
|
|
14
|
+
from .wrapping import PreparedText, TextLayout, TextWrapping, WrappedLine, layout_text, prepare_text
|
|
14
15
|
|
|
15
16
|
__all__ = [
|
|
16
17
|
"FontSpec",
|
|
@@ -22,4 +23,10 @@ __all__ = [
|
|
|
22
23
|
"TextRenderCommand",
|
|
23
24
|
"TextRenderer",
|
|
24
25
|
"TextSizeSpec",
|
|
26
|
+
"TextWrapping",
|
|
27
|
+
"PreparedText",
|
|
28
|
+
"TextLayout",
|
|
29
|
+
"WrappedLine",
|
|
30
|
+
"prepare_text",
|
|
31
|
+
"layout_text",
|
|
25
32
|
]
|
|
@@ -14,6 +14,7 @@ from .renderer import (
|
|
|
14
14
|
TextRenderer,
|
|
15
15
|
TextSizeSpec,
|
|
16
16
|
)
|
|
17
|
+
from .wrapping import PreparedText, TextWrapping, layout_text, prepare_text
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
@dataclass
|
|
@@ -31,7 +32,10 @@ class TextComponent(ComponentBase):
|
|
|
31
32
|
size: TextSizeSpec = field(default_factory=TextSizeSpec)
|
|
32
33
|
appearance: TextAppearance = field(default_factory=TextAppearance)
|
|
33
34
|
max_width_px: float | None = None
|
|
35
|
+
wrapping: TextWrapping | None = None
|
|
34
36
|
_visual_bounds_cache: BoundingBox | None = field(default=None, init=False, repr=False)
|
|
37
|
+
_prepared_text_cache: PreparedText | None = field(default=None, init=False, repr=False)
|
|
38
|
+
_prepared_text_key: tuple[object, ...] | None = field(default=None, init=False, repr=False)
|
|
35
39
|
|
|
36
40
|
def _resolved_frame(self) -> str:
|
|
37
41
|
return self.position.frame or self.default_frame
|
|
@@ -89,11 +93,79 @@ class TextComponent(ComponentBase):
|
|
|
89
93
|
*,
|
|
90
94
|
transformer: CoordinateTransformer | None = None,
|
|
91
95
|
) -> TextRenderBatch:
|
|
96
|
+
if self.wrapping is not None and self.max_width_px is not None:
|
|
97
|
+
batch = self._wrapped_render_batch(renderer, display, transformer=transformer)
|
|
98
|
+
renderer.draw_text_batch(batch)
|
|
99
|
+
return batch
|
|
92
100
|
command, _ = self.layout(renderer, display, transformer=transformer)
|
|
93
101
|
batch = TextRenderBatch(commands=(command,))
|
|
94
102
|
renderer.draw_text_batch(batch)
|
|
95
103
|
return batch
|
|
96
104
|
|
|
105
|
+
def _wrapped_render_batch(
|
|
106
|
+
self,
|
|
107
|
+
renderer: TextRenderer,
|
|
108
|
+
display: DisplayableArea,
|
|
109
|
+
*,
|
|
110
|
+
transformer: CoordinateTransformer | None = None,
|
|
111
|
+
) -> TextRenderBatch:
|
|
112
|
+
font_size_px = self.size.resolve_px(display)
|
|
113
|
+
key = (
|
|
114
|
+
id(renderer),
|
|
115
|
+
self.text,
|
|
116
|
+
self.font,
|
|
117
|
+
font_size_px,
|
|
118
|
+
self.appearance,
|
|
119
|
+
self.wrapping,
|
|
120
|
+
)
|
|
121
|
+
if self._prepared_text_cache is None or self._prepared_text_key != key:
|
|
122
|
+
def measure(value: str) -> float:
|
|
123
|
+
return renderer.measure_text(
|
|
124
|
+
TextMeasureRequest(
|
|
125
|
+
text=value,
|
|
126
|
+
font=self.font,
|
|
127
|
+
font_size_px=font_size_px,
|
|
128
|
+
appearance=self.appearance,
|
|
129
|
+
)
|
|
130
|
+
).width_px
|
|
131
|
+
|
|
132
|
+
self._prepared_text_cache = prepare_text(
|
|
133
|
+
self.text,
|
|
134
|
+
measure=measure,
|
|
135
|
+
wrapping=self.wrapping,
|
|
136
|
+
)
|
|
137
|
+
self._prepared_text_key = key
|
|
138
|
+
line_height = font_size_px * self.appearance.line_height_multiplier
|
|
139
|
+
wrapped = layout_text(
|
|
140
|
+
self._prepared_text_cache,
|
|
141
|
+
max_width_px=self.max_width_px,
|
|
142
|
+
line_height_px=line_height,
|
|
143
|
+
)
|
|
144
|
+
x, y = self._resolved_xy(transformer)
|
|
145
|
+
frame = self._resolved_frame()
|
|
146
|
+
commands = tuple(
|
|
147
|
+
TextRenderCommand(
|
|
148
|
+
component_id=f"{self.component_id}:line:{index}",
|
|
149
|
+
text=line.text,
|
|
150
|
+
x=x,
|
|
151
|
+
y=y + index * line_height,
|
|
152
|
+
frame=frame,
|
|
153
|
+
font=self.font,
|
|
154
|
+
font_size_px=font_size_px,
|
|
155
|
+
appearance=self.appearance,
|
|
156
|
+
max_width_px=None,
|
|
157
|
+
)
|
|
158
|
+
for index, line in enumerate(wrapped.lines)
|
|
159
|
+
)
|
|
160
|
+
self._visual_bounds_cache = BoundingBox(
|
|
161
|
+
x=x,
|
|
162
|
+
y=y,
|
|
163
|
+
width=wrapped.width_px,
|
|
164
|
+
height=wrapped.height_px,
|
|
165
|
+
frame=frame,
|
|
166
|
+
)
|
|
167
|
+
return TextRenderBatch(commands=commands)
|
|
168
|
+
|
|
97
169
|
def visual_bounds(self) -> BoundingBox:
|
|
98
170
|
if self._visual_bounds_cache is None:
|
|
99
171
|
return BoundingBox(
|