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.
Files changed (192) hide show
  1. {luvatrix-0.2.0/luvatrix.egg-info → luvatrix-0.2.2}/PKG-INFO +43 -1
  2. {luvatrix-0.2.0 → luvatrix-0.2.2}/README.md +42 -0
  3. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix/app.py +64 -23
  4. {luvatrix-0.2.0 → luvatrix-0.2.2/luvatrix.egg-info}/PKG-INFO +43 -1
  5. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix.egg-info/SOURCES.txt +3 -1
  6. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/__init__.py +1 -1
  7. luvatrix-0.2.2/luvatrix_core/core/interaction_work.py +162 -0
  8. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/android/runner.py +32 -4
  9. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/text/__init__.py +7 -0
  10. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/text/component.py +72 -0
  11. luvatrix-0.2.2/luvatrix_ui/text/wrapping.py +233 -0
  12. {luvatrix-0.2.0 → luvatrix-0.2.2}/pyproject.toml +1 -1
  13. {luvatrix-0.2.0 → luvatrix-0.2.2}/LICENSE +0 -0
  14. {luvatrix-0.2.0 → luvatrix-0.2.2}/MANIFEST.in +0 -0
  15. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix/__init__.py +0 -0
  16. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix/auth/__init__.py +0 -0
  17. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix/auth/calendar.py +0 -0
  18. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix/auth/google.py +0 -0
  19. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix/auth/sign_in.py +0 -0
  20. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix/auth/ui.py +0 -0
  21. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix.egg-info/dependency_links.txt +0 -0
  22. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix.egg-info/entry_points.txt +0 -0
  23. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix.egg-info/requires.txt +0 -0
  24. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix.egg-info/top_level.txt +0 -0
  25. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/accel.py +0 -0
  26. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/__init__.py +0 -0
  27. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/app_runtime.py +0 -0
  28. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/audit.py +0 -0
  29. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/coordinates.py +0 -0
  30. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/debug_capture.py +0 -0
  31. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/debug_menu.py +0 -0
  32. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/display_runtime.py +0 -0
  33. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/energy_safety.py +0 -0
  34. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/engine.py +0 -0
  35. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/events.py +0 -0
  36. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/frame_rate_controller.py +0 -0
  37. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/hdi_thread.py +0 -0
  38. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/process_runtime.py +0 -0
  39. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/process_sdk.py +0 -0
  40. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/protocol_governance.py +0 -0
  41. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/scene_display_runtime.py +0 -0
  42. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/scene_graph.py +0 -0
  43. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/scene_rasterizer.py +0 -0
  44. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/sensor_manager.py +0 -0
  45. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/ui_frame_renderer.py +0 -0
  46. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/unified_runtime.py +0 -0
  47. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/core/window_matrix.py +0 -0
  48. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/perf/copy_telemetry.py +0 -0
  49. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/__init__.py +0 -0
  50. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/android/__init__.py +0 -0
  51. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/android/camera_processing_contract.py +0 -0
  52. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/android/camera_style.py +0 -0
  53. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/android/hdi_source.py +0 -0
  54. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/android/scene_target.py +0 -0
  55. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/android/sensors.py +0 -0
  56. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/android/vulkan_target.py +0 -0
  57. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/frame_pipeline.py +0 -0
  58. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/ios/__init__.py +0 -0
  59. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/ios/hdi_source.py +0 -0
  60. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/ios/lifecycle.py +0 -0
  61. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/ios/metal_backend.py +0 -0
  62. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/ios/runner.py +0 -0
  63. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/ios/scene_target.py +0 -0
  64. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/ios/window_system.py +0 -0
  65. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/macos/__init__.py +0 -0
  66. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/macos/hdi_source.py +0 -0
  67. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/macos/iohid_source.py +0 -0
  68. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/macos/metal_backend.py +0 -0
  69. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/macos/metal_presenter.py +0 -0
  70. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/macos/metal_scene_target.py +0 -0
  71. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/macos/sensors.py +0 -0
  72. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/macos/vulkan_backend.py +0 -0
  73. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/macos/vulkan_presenter.py +0 -0
  74. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/macos/window_system.py +0 -0
  75. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/package_sync.py +0 -0
  76. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/vulkan_compat.py +0 -0
  77. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/vulkan_scaling.py +0 -0
  78. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/vulkan_setup.py +0 -0
  79. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/web/__init__.py +0 -0
  80. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/web/build.py +0 -0
  81. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/web/command_buffer.py +0 -0
  82. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/web/python_shim/luvatrix/__init__.py +0 -0
  83. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/web/python_shim/luvatrix/app.py +0 -0
  84. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/web/runtime_assets/index.html +0 -0
  85. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/web/runtime_assets/runtime/command-buffer.js +0 -0
  86. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/web/runtime_assets/runtime/input.js +0 -0
  87. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/web/runtime_assets/runtime/luvatrix-web.js +0 -0
  88. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/web/runtime_assets/runtime/package.json +0 -0
  89. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/web/runtime_assets/runtime/renderers.js +0 -0
  90. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/web/runtime_assets/runtime/spotify-bridge.js +0 -0
  91. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/web/server.py +0 -0
  92. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/platform/web/websocket_target.py +0 -0
  93. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/render/__init__.py +0 -0
  94. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/render/framebuffer.py +0 -0
  95. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/render/svg.py +0 -0
  96. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/scaffold.py +0 -0
  97. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/targets/__init__.py +0 -0
  98. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/targets/base.py +0 -0
  99. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/targets/cpu_scene_target.py +0 -0
  100. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/targets/metal_target.py +0 -0
  101. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/targets/scene_target.py +0 -0
  102. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/targets/vulkan_target.py +0 -0
  103. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/targets/web_target.py +0 -0
  104. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/.gitignore +0 -0
  105. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/build.gradle.kts +0 -0
  106. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/AndroidManifest.xml +0 -0
  107. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/assets/luvatrix_bitmap_font.txt +0 -0
  108. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/assets/luvatrix_launch_config.json +0 -0
  109. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/assets/luvatrix_matrix_font_alpha.txt +0 -0
  110. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/cpp/CMakeLists.txt +0 -0
  111. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/cpp/luvatrix_camera_preview.frag +0 -0
  112. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/cpp/luvatrix_camera_preview_shaders.h +0 -0
  113. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/cpp/luvatrix_vulkan_renderer.cpp +0 -0
  114. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/java/com/luvatrix/app/CameraBridge.kt +0 -0
  115. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/java/com/luvatrix/app/LuvatrixVulkanView.kt +0 -0
  116. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/java/com/luvatrix/app/MainActivity.kt +0 -0
  117. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/java/com/luvatrix/app/NativeVulkan.kt +0 -0
  118. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/python/.gitignore +0 -0
  119. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/python/luvatrix_android_boot.py +0 -0
  120. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/python/luvatrix_launch_config.json +0 -0
  121. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  122. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  123. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  124. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  125. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  126. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/app/src/main/res/values/styles.xml +0 -0
  127. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/build.gradle.kts +0 -0
  128. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/gradle.properties +0 -0
  129. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/gradlew +0 -0
  130. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/scripts/emulator_acceptance.sh +0 -0
  131. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/scripts/sync_python_assets.sh +0 -0
  132. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/android/settings.gradle.kts +0 -0
  133. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/ios/Luvatrix/AppDelegate.swift +0 -0
  134. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/ios/Luvatrix/Assets.xcassets/AppIcon.appiconset/Contents.json +0 -0
  135. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/ios/Luvatrix/Assets.xcassets/AppIcon.appiconset/icon.png +0 -0
  136. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/ios/Luvatrix/Assets.xcassets/Contents.json +0 -0
  137. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/ios/Luvatrix/Info.plist +0 -0
  138. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/ios/Luvatrix/Luvatrix-Bridging-Header.h +0 -0
  139. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/ios/Luvatrix/Luvatrix.entitlements +0 -0
  140. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/ios/Luvatrix/PythonBridge.h +0 -0
  141. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/ios/Luvatrix/PythonBridge.m +0 -0
  142. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/ios/Luvatrix/main.swift +0 -0
  143. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/ios/project.yml +0 -0
  144. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/templates/native/ios/scripts/setup_ios.sh +0 -0
  145. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/ui/__init__.py +0 -0
  146. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/ui/element.py +0 -0
  147. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_core/ui/page_loader.py +0 -0
  148. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/__init__.py +0 -0
  149. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/adapters/__init__.py +0 -0
  150. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/adapters/normalize.py +0 -0
  151. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/api.py +0 -0
  152. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/compile/__init__.py +0 -0
  153. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/compile/app_protocol.py +0 -0
  154. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/display.py +0 -0
  155. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/dynamic_axis.py +0 -0
  156. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/errors.py +0 -0
  157. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/figure.py +0 -0
  158. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/live.py +0 -0
  159. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/raster/__init__.py +0 -0
  160. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/raster/canvas.py +0 -0
  161. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/raster/draw_lines.py +0 -0
  162. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/raster/draw_markers.py +0 -0
  163. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/raster/draw_text.py +0 -0
  164. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/raster/layers.py +0 -0
  165. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/scales.py +0 -0
  166. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_plot/series.py +0 -0
  167. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/__init__.py +0 -0
  168. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/component_schema.py +0 -0
  169. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/controls/__init__.py +0 -0
  170. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/controls/button.py +0 -0
  171. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/controls/interaction.py +0 -0
  172. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/controls/stained_glass_button.py +0 -0
  173. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/controls/svg_component.py +0 -0
  174. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/controls/svg_renderer.py +0 -0
  175. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/planes_protocol.py +0 -0
  176. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/planes_runtime.py +0 -0
  177. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/planes_v2_validator.py +0 -0
  178. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/planning/__init__.py +0 -0
  179. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/planning/agile_renderer.py +0 -0
  180. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/planning/exporters.py +0 -0
  181. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/planning/gantt_renderer.py +0 -0
  182. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/planning/interaction.py +0 -0
  183. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/planning/schema.py +0 -0
  184. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/planning/validation.py +0 -0
  185. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/style/__init__.py +0 -0
  186. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/style/theme.py +0 -0
  187. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/table/__init__.py +0 -0
  188. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/table/component.py +0 -0
  189. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/text/renderer.py +0 -0
  190. {luvatrix-0.2.0 → luvatrix-0.2.2}/luvatrix_ui/ui_ir.py +0 -0
  191. {luvatrix-0.2.0 → luvatrix-0.2.2}/main.py +0 -0
  192. {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.0
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
- cursor = int(x)
1279
- top = int(y)
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 raw_ch in str(text):
1283
- glyph = font.glyphs.get(raw_ch, font.glyphs.get(raw_ch.upper(), space))
1284
- for gy, row in enumerate(glyph):
1285
- for gx, coverage in enumerate(row):
1286
- if not antialias:
1287
- coverage = 255 if coverage >= 128 else 0
1288
- if coverage <= 0:
1289
- continue
1290
- _paint_matrix_rect(
1291
- matrix,
1292
- cursor + gx * scale,
1293
- top + gy * scale,
1294
- cursor + (gx + 1) * scale,
1295
- top + (gy + 1) * scale,
1296
- rgba,
1297
- coverage=coverage,
1298
- width=width,
1299
- height=height,
1300
- )
1301
- cursor += font.advance * scale
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.0
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
@@ -1,4 +1,4 @@
1
1
  """Luvatrix runtime."""
2
2
 
3
3
  __all__ = ["__version__"]
4
- __version__ = "0.2.0"
4
+ __version__ = "0.2.2"
@@ -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
- imports = _top_level_imports_for_app(app_dir, exclude_dirs=exclude_dirs)
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 _top_level_imports_for_app(app_dir: Path, *, exclude_dirs: Iterable[Path] = ()) -> set[str]:
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.split(".", 1)[0])
361
+ imports.add(alias.name)
359
362
  elif isinstance(node, ast.ImportFrom) and node.module:
360
- imports.add(node.module.split(".", 1)[0])
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(