meltygui 0.1.0__py3-none-any.whl

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 (372) hide show
  1. meltygui/__init__.py +107 -0
  2. meltygui/accounts/__init__.py +0 -0
  3. meltygui/accounts/internet_accounts.py +1355 -0
  4. meltygui/chat/__init__.py +91 -0
  5. meltygui/chat/activity.py +75 -0
  6. meltygui/chat/backends.py +36 -0
  7. meltygui/chat/chat_interface.py +732 -0
  8. meltygui/chat/chat_proxy.py +352 -0
  9. meltygui/chat/codex_proxy.py +592 -0
  10. meltygui/chat/codex_settings.py +100 -0
  11. meltygui/chat/codex_transport.py +60 -0
  12. meltygui/chat/command_parser.py +204 -0
  13. meltygui/chat/images.py +227 -0
  14. meltygui/chat/messages.py +417 -0
  15. meltygui/chat/metadata.py +139 -0
  16. meltygui/chat/writer_locks.py +64 -0
  17. meltygui/code/__init__.py +0 -0
  18. meltygui/code/basic_converters.py +533 -0
  19. meltygui/code/chain_converters.py +2111 -0
  20. meltygui/code/code_checks.py +2209 -0
  21. meltygui/code/core_syntax.py +1430 -0
  22. meltygui/code/file_converters.py +1933 -0
  23. meltygui/code/fileref.py +702 -0
  24. meltygui/code/hotswap_guard.py +144 -0
  25. meltygui/code/libcst_conversion.py +9724 -0
  26. meltygui/code/live_instrument.py +392 -0
  27. meltygui/code/live_view.py +2490 -0
  28. meltygui/code/melty_scan.py +2684 -0
  29. meltygui/code/new_codecs.py +1255 -0
  30. meltygui/code/new_converters.py +3017 -0
  31. meltygui/code/project_code.py +278 -0
  32. meltygui/code/source_context.py +63 -0
  33. meltygui/code/symbol_roster.py +1588 -0
  34. meltygui/code/syntax_check.py +34 -0
  35. meltygui/code/syntax_check_worker.py +114 -0
  36. meltygui/completion/__init__.py +0 -0
  37. meltygui/completion/fim.py +1232 -0
  38. meltygui/completion/fim_context.py +481 -0
  39. meltygui/completion/providers/__init__.py +0 -0
  40. meltygui/completion/providers/anthropic_oauth.py +446 -0
  41. meltygui/completion/providers/anthropic_requests.py +69 -0
  42. meltygui/completion/providers/claude.py +203 -0
  43. meltygui/completion/providers/claude_usage.py +499 -0
  44. meltygui/completion/providers/codex_accounts.py +180 -0
  45. meltygui/completion/providers/copilot.py +617 -0
  46. meltygui/completion/providers/oauth_popup.py +220 -0
  47. meltygui/completion/providers/ollama.py +320 -0
  48. meltygui/completion/providers/profiles.py +23 -0
  49. meltygui/core/README.md +88 -0
  50. meltygui/core/__init__.py +1 -0
  51. meltygui/core/automation/__init__.py +1 -0
  52. meltygui/core/automation/action_core.py +153 -0
  53. meltygui/core/automation/collection_action.py +45 -0
  54. meltygui/core/automation/mcp_eval.py +167 -0
  55. meltygui/core/automation/mcp_hotswap.py +107 -0
  56. meltygui/core/automation/mcp_query.py +552 -0
  57. meltygui/core/automation/mcp_server.py +657 -0
  58. meltygui/core/automation/orchestration_core.py +2636 -0
  59. meltygui/core/automation/query_core.py +124 -0
  60. meltygui/core/automation/search_core.py +26 -0
  61. meltygui/core/automation/selector_core.py +340 -0
  62. meltygui/core/automation/value_core.py +1752 -0
  63. meltygui/core/cache/__init__.py +1 -0
  64. meltygui/core/cache/cache_diagnostics.py +0 -0
  65. meltygui/core/cache/invalidation_decoration.py +153 -0
  66. meltygui/core/cache/invalidation_tracker.py +43 -0
  67. meltygui/core/cache/tile_cache.py +5968 -0
  68. meltygui/core/conversion/__init__.py +1 -0
  69. meltygui/core/conversion/bubbling.py +599 -0
  70. meltygui/core/conversion/cache_tree.py +188 -0
  71. meltygui/core/conversion/chain.py +113 -0
  72. meltygui/core/conversion/converter_register.py +145 -0
  73. meltygui/core/conversion/data_decoration.py +67 -0
  74. meltygui/core/conversion/dict_conversion.py +1815 -0
  75. meltygui/core/conversion/dict_conversion_util.py +177 -0
  76. meltygui/core/conversion/dynamic_obj.py +89 -0
  77. meltygui/core/conversion/graph_compare.py +183 -0
  78. meltygui/core/conversion/load_save_v2.py +1032 -0
  79. meltygui/core/conversion/missing_saved_class.py +39 -0
  80. meltygui/core/conversion/path_finder.py +606 -0
  81. meltygui/core/conversion/render_host.py +1083 -0
  82. meltygui/core/core_render.py +6537 -0
  83. meltygui/core/definition_hotswap.py +231 -0
  84. meltygui/core/diagnostics/__init__.py +1 -0
  85. meltygui/core/diagnostics/attribute_churn.py +38 -0
  86. meltygui/core/diagnostics/fps_counter.py +39 -0
  87. meltygui/core/diagnostics/gpu_frame_timer.py +103 -0
  88. meltygui/core/diagnostics/inspection_core.py +169 -0
  89. meltygui/core/diagnostics/monitor_core.py +105 -0
  90. meltygui/core/diagnostics/notifications.py +706 -0
  91. meltygui/core/diagnostics/perf_trace.py +281 -0
  92. meltygui/core/diagnostics/profile_decoration.py +88 -0
  93. meltygui/core/diagnostics/resize_trace.py +62 -0
  94. meltygui/core/diagnostics/screenshot_core.py +247 -0
  95. meltygui/core/diagnostics/session_status.py +98 -0
  96. meltygui/core/diagnostics/trace_core.py +445 -0
  97. meltygui/core/files/__init__.py +1 -0
  98. meltygui/core/files/file_core.py +208 -0
  99. meltygui/core/files/file_explorer_core.py +104 -0
  100. meltygui/core/files/file_tree_core.py +198 -0
  101. meltygui/core/files/file_watch_core.py +43 -0
  102. meltygui/core/files/import_graph_core.py +43 -0
  103. meltygui/core/files/metadata_core.py +51 -0
  104. meltygui/core/graphics/__init__.py +1 -0
  105. meltygui/core/graphics/cuda_context_core.py +166 -0
  106. meltygui/core/graphics/cuda_interop_core.py +136 -0
  107. meltygui/core/graphics/cuda_kernel_core.py +91 -0
  108. meltygui/core/graphics/framebuffer_recorder.py +337 -0
  109. meltygui/core/graphics/gl_state.py +658 -0
  110. meltygui/core/graphics/lut_core.py +52 -0
  111. meltygui/core/graphics/overlay_renderer.py +984 -0
  112. meltygui/core/graphics/scene_target.py +180 -0
  113. meltygui/core/graphics/screenshot.py +439 -0
  114. meltygui/core/graphics/shader_func.py +478 -0
  115. meltygui/core/graphics/tensor_core.py +45 -0
  116. meltygui/core/graphics/text_texture.py +329 -0
  117. meltygui/core/graphics/wayland_color.py +635 -0
  118. meltygui/core/input/__init__.py +1 -0
  119. meltygui/core/input/collision.py +165 -0
  120. meltygui/core/input/drag_drop_core.py +1525 -0
  121. meltygui/core/input/hypr_left_drag.py +323 -0
  122. meltygui/core/input/input_core.py +245 -0
  123. meltygui/core/input/input_handler.py +1101 -0
  124. meltygui/core/input/mouse_cursor.py +355 -0
  125. meltygui/core/input/pynput_backend.py +1054 -0
  126. meltygui/core/input/space_mouse.py +338 -0
  127. meltygui/core/input/touchpad_backend.py +393 -0
  128. meltygui/core/input/view_selection.py +177 -0
  129. meltygui/core/layout/__init__.py +1 -0
  130. meltygui/core/layout/column_core.py +2153 -0
  131. meltygui/core/layout/cursor_core.py +161 -0
  132. meltygui/core/layout/dropdown_core.py +278 -0
  133. meltygui/core/layout/edge_constraints.py +155 -0
  134. meltygui/core/layout/grid_core.py +117 -0
  135. meltygui/core/layout/header_core.py +23 -0
  136. meltygui/core/layout/header_runtime.py +67 -0
  137. meltygui/core/layout/layout_core.py +87 -0
  138. meltygui/core/layout/tile_manager_core.py +591 -0
  139. meltygui/core/melty.py +6726 -0
  140. meltygui/core/module_map.json +896 -0
  141. meltygui/core/module_names.py +19 -0
  142. meltygui/core/rendering/__init__.py +1 -0
  143. meltygui/core/rendering/core_decoration.py +431 -0
  144. meltygui/core/rendering/core_render_helpers.py +328 -0
  145. meltygui/core/rendering/func_metadata.py +398 -0
  146. meltygui/core/rendering/mode.py +818 -0
  147. meltygui/core/rendering/mode_defaults.py +41 -0
  148. meltygui/core/rendering/modes.py +136 -0
  149. meltygui/core/rendering/parameter_core.py +1665 -0
  150. meltygui/core/rendering/render_dispatch.py +1891 -0
  151. meltygui/core/rendering/render_funcs.py +273 -0
  152. meltygui/core/rendering/shaped.py +312 -0
  153. meltygui/core/rendering/window_decoration.py +25 -0
  154. meltygui/core/runtime/__init__.py +1 -0
  155. meltygui/core/runtime/app.py +735 -0
  156. meltygui/core/runtime/app_session.py +140 -0
  157. meltygui/core/runtime/background.py +564 -0
  158. meltygui/core/runtime/extensions.py +53 -0
  159. meltygui/core/runtime/gc_manager.py +1163 -0
  160. meltygui/core/runtime/lifecycle.py +21 -0
  161. meltygui/core/runtime/paths.py +27 -0
  162. meltygui/core/runtime/settings.py +14 -0
  163. meltygui/core/runtime/singleton.py +16 -0
  164. meltygui/core/runtime/thread_safe_bool.py +24 -0
  165. meltygui/core/runtime/thread_signal.py +30 -0
  166. meltygui/core/runtime/toggles.py +3115 -0
  167. meltygui/core/services/__init__.py +1 -0
  168. meltygui/core/services/account_core.py +10 -0
  169. meltygui/core/services/chat_core.py +19 -0
  170. meltygui/core/services/claude_terminal_core.py +346 -0
  171. meltygui/core/services/terminal_core.py +458 -0
  172. meltygui/core/services/terminal_runtime.py +78 -0
  173. meltygui/core/styling/__init__.py +1 -0
  174. meltygui/core/styling/color_core.py +46 -0
  175. meltygui/core/styling/fonts.py +639 -0
  176. meltygui/core/styling/global_style.py +338 -0
  177. meltygui/core/styling/style.py +198 -0
  178. meltygui/core/styling/style_core.py +522 -0
  179. meltygui/core/styling/warm_start.py +149 -0
  180. meltygui/core/windowing/__init__.py +1 -0
  181. meltygui/core/windowing/backends/PYIMGUI_LICENSE +28 -0
  182. meltygui/core/windowing/backends/__init__.py +1 -0
  183. meltygui/core/windowing/backends/imgui_renderer.py +138 -0
  184. meltygui/core/windowing/backends/native_wayland.py +850 -0
  185. meltygui/core/windowing/backends/protocols/xdg-decoration-unstable-v1.xml +156 -0
  186. meltygui/core/windowing/backends/protocols/xdg-shell.xml +1420 -0
  187. meltygui/core/windowing/backends/wayland_protocol.py +101 -0
  188. meltygui/core/windowing/dock_core.py +182 -0
  189. meltygui/core/windowing/frame_geometry.py +42 -0
  190. meltygui/core/windowing/geometry_feed.py +864 -0
  191. meltygui/core/windowing/glfw_utils.py +1343 -0
  192. meltygui/core/windowing/os_frame.py +1552 -0
  193. meltygui/core/windowing/surface.py +643 -0
  194. meltygui/core/windowing/titlebar.py +1560 -0
  195. meltygui/core/windowing/titlebar_buttons.py +281 -0
  196. meltygui/core/windowing/wayland_move.py +932 -0
  197. meltygui/core/windowing/window_api.py +62 -0
  198. meltygui/core/windowing/window_constants.py +339 -0
  199. meltygui/core/windowing/window_visibility.py +162 -0
  200. meltygui/debug/__init__.py +0 -0
  201. meltygui/debug/app_view_utils.py +9 -0
  202. meltygui/editor/__init__.py +0 -0
  203. meltygui/editor/bash_syntax.py +30 -0
  204. meltygui/editor/code_line_fast.py +152 -0
  205. meltygui/editor/diff.py +139 -0
  206. meltygui/editor/external_changes.py +159 -0
  207. meltygui/editor/file_header.py +41 -0
  208. meltygui/editor/live_usage.py +169 -0
  209. meltygui/editor/live_view_views.py +1803 -0
  210. meltygui/editor/pending_save.py +1351 -0
  211. meltygui/editor/roster_tints.py +547 -0
  212. meltygui/editor/source_preview.py +16 -0
  213. meltygui/editor/source_tools.py +10 -0
  214. meltygui/editor/source_ui.py +70 -0
  215. meltygui/editor/spell_check.py +77 -0
  216. meltygui/editor/text_editor.py +9076 -0
  217. meltygui/editor/usage_picker.py +538 -0
  218. meltygui/events/__init__.py +0 -0
  219. meltygui/events/example.py +118 -0
  220. meltygui/examples/__init__.py +0 -0
  221. meltygui/examples/columns_demo.py +33 -0
  222. meltygui/examples/columns_window_demo.py +60 -0
  223. meltygui/examples/context_menu_demo.py +24 -0
  224. meltygui/examples/context_menu_window_demo.py +49 -0
  225. meltygui/examples/gui_playground.py +127 -0
  226. meltygui/examples/live_view_playground.py +256 -0
  227. meltygui/examples/lora.py +43 -0
  228. meltygui/examples/lora_data.py +59 -0
  229. meltygui/examples/lora_policies.py +67 -0
  230. meltygui/examples/mode_demo.py +49 -0
  231. meltygui/examples/modifies_demo.py +119 -0
  232. meltygui/examples/scalar_policies.py +60 -0
  233. meltygui/examples/style_layouts.py +149 -0
  234. meltygui/examples/tile_manager_demo.py +74 -0
  235. meltygui/examples/tint_demo.py +160 -0
  236. meltygui/examples/tint_functions.py +56 -0
  237. meltygui/examples/trace_demo.py +88 -0
  238. meltygui/examples/two_windows.py +36 -0
  239. meltygui/files/__init__.py +0 -0
  240. meltygui/files/fast_file_explorer.py +439 -0
  241. meltygui/gnome_extension/lsd-window-geometry@latent-descent/extension.js +237 -0
  242. meltygui/gnome_extension/lsd-window-geometry@latent-descent/lsd-window-geometry@latent-descent.iml +9 -0
  243. meltygui/gnome_extension/lsd-window-geometry@latent-descent/metadata.json +7 -0
  244. meltygui/graphics/__init__.py +6 -0
  245. meltygui/graphics/base.py +85 -0
  246. meltygui/graphics/examples.py +507 -0
  247. meltygui/graphics/executor.py +520 -0
  248. meltygui/graphics/filter.py +667 -0
  249. meltygui/graphics/filter.pyi +856 -0
  250. meltygui/graphics/generate_stubs.py +22 -0
  251. meltygui/graphics/registry.py +203 -0
  252. meltygui/graphics/shader_compiler.py +155 -0
  253. meltygui/graphics/shaders.py +1302 -0
  254. meltygui/graphics/stub_generator.py +250 -0
  255. meltygui/graphics/texture_manager.py +170 -0
  256. meltygui/graphics/texture_min_max.py +295 -0
  257. meltygui/hdr_color.py +757 -0
  258. meltygui/image_load.py +308 -0
  259. meltygui/model/__init__.py +1 -0
  260. meltygui/model/account_model.py +142 -0
  261. meltygui/model/camera_model.py +159 -0
  262. meltygui/model/chat_model.py +86 -0
  263. meltygui/model/code_model.py +62 -0
  264. meltygui/model/code_proxy_model.py +876 -0
  265. meltygui/model/collection_model.py +41 -0
  266. meltygui/model/color_model.py +127 -0
  267. meltygui/model/cuda_tensor_model.py +36 -0
  268. meltygui/model/cuda_texture_model.py +149 -0
  269. meltygui/model/dropdown_model.py +137 -0
  270. meltygui/model/file_metadata_model.py +73 -0
  271. meltygui/model/file_model.py +289 -0
  272. meltygui/model/format_model.py +390 -0
  273. meltygui/model/graph_model.py +98 -0
  274. meltygui/model/icon_model.py +1024 -0
  275. meltygui/model/import_graph_model.py +468 -0
  276. meltygui/model/layout_model.py +15 -0
  277. meltygui/model/lut_model.py +266 -0
  278. meltygui/model/search_model.py +173 -0
  279. meltygui/model/tensor_model.py +402 -0
  280. meltygui/model/terminal_model.py +329 -0
  281. meltygui/model/texture_model.py +146 -0
  282. meltygui/model/tile_model.py +24 -0
  283. meltygui/model/trace_model.py +198 -0
  284. meltygui/model/trace_report_model.py +146 -0
  285. meltygui/models/__init__.py +0 -0
  286. meltygui/models/file_meta.py +613 -0
  287. meltygui/models/function_console.py +184 -0
  288. meltygui/models/orchestration.py +48 -0
  289. meltygui/pbr.py +1576 -0
  290. meltygui/png_unfilter.c +49 -0
  291. meltygui/resources/JetBrainsMono-Regular.ttf +0 -0
  292. meltygui/resources/THIRD_PARTY_NOTICES.md +21 -0
  293. meltygui/resources/dejavu/DejaVuSans-Bold.ttf +0 -0
  294. meltygui/resources/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
  295. meltygui/resources/dejavu/DejaVuSans.ttf +0 -0
  296. meltygui/resources/dejavu/LICENSE.txt +78 -0
  297. meltygui/resources/fontawesome-LICENSE.txt +121 -0
  298. meltygui/resources/fontawesome-webfont.ttf +0 -0
  299. meltygui/resources/hdri/studio_small_09_1k.hdr +0 -0
  300. meltygui/resources/jetbrains-weights/JetBrainsMono-Bold.ttf +0 -0
  301. meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraBold.ttf +0 -0
  302. meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraLight.ttf +0 -0
  303. meltygui/resources/jetbrains-weights/JetBrainsMono-Light.ttf +0 -0
  304. meltygui/resources/jetbrains-weights/JetBrainsMono-Medium.ttf +0 -0
  305. meltygui/resources/jetbrains-weights/JetBrainsMono-SemiBold.ttf +0 -0
  306. meltygui/resources/jetbrains-weights/JetBrainsMono-Thin.ttf +0 -0
  307. meltygui/resources/jetbrains-weights/OFL.txt +93 -0
  308. meltygui/resources/jetbrains-weights/README.md +2 -0
  309. meltygui/state/__init__.py +0 -0
  310. meltygui/state/account_state.py +22 -0
  311. meltygui/state/animation_state.py +97 -0
  312. meltygui/state/annotation_state.py +22 -0
  313. meltygui/state/chat_state.py +36 -0
  314. meltygui/state/code_state.py +10 -0
  315. meltygui/state/core_enums.py +59 -0
  316. meltygui/state/core_markers.py +115 -0
  317. meltygui/state/core_undo.py +1075 -0
  318. meltygui/state/file_state.py +75 -0
  319. meltygui/state/graph_state.py +26 -0
  320. meltygui/state/inspection_state.py +47 -0
  321. meltygui/state/menu_state.py +10 -0
  322. meltygui/state/model_enums.py +11 -0
  323. meltygui/state/new_core_model.py +2394 -0
  324. meltygui/state/orchestration_state.py +14 -0
  325. meltygui/state/query_state.py +24 -0
  326. meltygui/state/tensor_state.py +10 -0
  327. meltygui/state/terminal_state.py +23 -0
  328. meltygui/state/trace_state.py +32 -0
  329. meltygui/state/voxel_state.py +12 -0
  330. meltygui/text_index.py +816 -0
  331. meltygui/utils/__init__.py +0 -0
  332. meltygui/utils/jump_to_code.py +344 -0
  333. meltygui/utils/pkl_inspect.py +90 -0
  334. meltygui/utils/render_utils.py +1419 -0
  335. meltygui/view/__init__.py +1 -0
  336. meltygui/view/account_view.py +524 -0
  337. meltygui/view/action_view.py +73 -0
  338. meltygui/view/chat_decoration_view.py +112 -0
  339. meltygui/view/chat_view.py +1703 -0
  340. meltygui/view/code_view.py +2677 -0
  341. meltygui/view/collection_view.py +1185 -0
  342. meltygui/view/color_view.py +824 -0
  343. meltygui/view/control_view.py +559 -0
  344. meltygui/view/decoration_view.py +439 -0
  345. meltygui/view/diagnostic_view.py +177 -0
  346. meltygui/view/dropdown_view.py +1082 -0
  347. meltygui/view/file_view.py +1599 -0
  348. meltygui/view/graph_cuda_view.py +105 -0
  349. meltygui/view/graph_view.py +882 -0
  350. meltygui/view/header_view.py +848 -0
  351. meltygui/view/input_view.py +238 -0
  352. meltygui/view/inspection_view.py +1783 -0
  353. meltygui/view/layout_view.py +596 -0
  354. meltygui/view/lut_view.py +45 -0
  355. meltygui/view/menu_view.py +212 -0
  356. meltygui/view/orchestration_view.py +658 -0
  357. meltygui/view/query_view.py +118 -0
  358. meltygui/view/search_view.py +342 -0
  359. meltygui/view/tab_view.py +258 -0
  360. meltygui/view/tensor_view.py +430 -0
  361. meltygui/view/terminal_view.py +355 -0
  362. meltygui/view/text_view.py +8031 -0
  363. meltygui/view/texture_view.py +480 -0
  364. meltygui/view/tile_view.py +38 -0
  365. meltygui/view/trace_view.py +923 -0
  366. meltygui/view/voxel_cuda_view.py +824 -0
  367. meltygui/view/voxel_view.py +1860 -0
  368. meltygui/view/window_view.py +111 -0
  369. meltygui-0.1.0.dist-info/METADATA +143 -0
  370. meltygui-0.1.0.dist-info/RECORD +372 -0
  371. meltygui-0.1.0.dist-info/WHEEL +4 -0
  372. meltygui-0.1.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,161 @@
1
+ import inspect
2
+ import sys
3
+
4
+ import meltygui_imgui as imgui
5
+ from meltygui.hdr_color import pack_color
6
+
7
+ from meltygui.core.conversion.dict_conversion import DictConversion
8
+ from meltygui.core.runtime.toggles import Toggles
9
+ from meltygui.utils.render_utils import LSDView
10
+ from meltygui.utils.render_utils import print_stack_trace
11
+ from meltygui.utils.render_utils import push_style_color
12
+ from meltygui.utils.render_utils import pop_style_color
13
+ from meltygui.utils.render_utils import print_colored_traceback
14
+ from meltygui.core.melty import Melty
15
+
16
+
17
+ def set_cursor_pos_y(pos_y):
18
+ current_y = imgui.get_cursor_pos_y()
19
+ imgui.set_cursor_pos_y(pos_y)
20
+ height = imgui.get_cursor_pos_y() - current_y
21
+ if Toggles.show_line_breaks:
22
+ if draw_rect(width=5, height=None, color=(1.0, 0.0, 1.0, 0.8)):
23
+ print_stack_trace(skip=-2)
24
+
25
+
26
+ def set_cursor_pos_x(pos_x):
27
+ current_x = imgui.get_cursor_pos_x()
28
+ imgui.set_cursor_pos_x(pos_x)
29
+ width = imgui.get_cursor_pos_x() - current_x
30
+ if Toggles.show_line_breaks:
31
+ if draw_rect(width=5, height=None, color=(1.0, 0.0, 1.0, 0.8)):
32
+ print_stack_trace(skip=-2)
33
+
34
+
35
+ def set_cursor_screen_pos(pos):
36
+ imgui.set_cursor_screen_pos(pos)
37
+ if Toggles.show_line_breaks:
38
+ if draw_rect(width=5, height=None, color=(1.0, 0.0, 1.0, 0.8)):
39
+ print_stack_trace(skip=-2)
40
+
41
+
42
+ def set_cursor_screen_position(pos):
43
+ imgui.set_cursor_screen_position(pos)
44
+ if Toggles.show_line_breaks:
45
+ if draw_rect(width=5, height=None, color=(1.0, 0.0, 1.0, 0.8)):
46
+ print_stack_trace(skip=-2)
47
+
48
+
49
+ def set_cursor_pos(pos):
50
+ current_pos = imgui.get_cursor_pos()
51
+ imgui.set_cursor_pos(pos)
52
+ if Toggles.show_line_breaks:
53
+ if draw_rect(width=5, height=None, color=(1.0, 0.0, 1.0, 0.8)):
54
+ print_stack_trace(skip=-2)
55
+
56
+
57
+ def spacing():
58
+ imgui.spacing()
59
+ if Toggles.show_line_breaks:
60
+ if draw_rect(width=3, height=None, color=(0, 0.1, 0.7, 0.8)):
61
+ print_stack_trace(skip=-2)
62
+
63
+
64
+ def indent(indent_size=None, attr_name=None):
65
+ if Toggles.show_line_breaks:
66
+ if draw_rect(width=3, height=None, color=(0.1, 0.1, 1.0, 0.8)):
67
+ print_stack_trace(skip=-2)
68
+ if attr_name is not None:
69
+ print(f"=======Indenting for {attr_name}=========")
70
+
71
+ if not (is_hovered(width=5, height=None) and imgui.is_mouse_down(imgui.MOUSE_BUTTON_MIDDLE)):
72
+ imgui.indent(indent_size)
73
+ else:
74
+ imgui.indent(indent_size)
75
+
76
+
77
+ def same_line(spacing=None):
78
+ if Toggles.show_line_breaks:
79
+ if draw_rect(width=3, height=None, color=(1.0, 0.5, 0.1, 0.5)):
80
+ print_stack_trace(skip=-2)
81
+ if not (is_hovered(width=3, height=None) and imgui.is_mouse_down(imgui.MOUSE_BUTTON_MIDDLE)):
82
+ if spacing is None:
83
+ imgui.same_line()
84
+ else:
85
+ imgui.same_line(spacing=spacing)
86
+ else:
87
+ if spacing is None:
88
+ imgui.same_line()
89
+ else:
90
+ imgui.same_line(spacing=spacing)
91
+
92
+
93
+ def new_line():
94
+ if Toggles.show_line_breaks:
95
+ if draw_rect(width=3, height=None, color=(0.5, 0, 0, 0.5)):
96
+ print_stack_trace(skip=-2)
97
+ if not (is_hovered(width=3, height=None) and imgui.is_mouse_down(imgui.MOUSE_BUTTON_MIDDLE)):
98
+ imgui.new_line()
99
+ else:
100
+ imgui.new_line()
101
+
102
+
103
+
104
+ def copy_attributes(source, target):
105
+ """
106
+ Copies attributes from source to target, excluding private attributes.
107
+ If an attribute exists in both, it will be overwritten in the target.
108
+ """
109
+ if not isinstance(source, DictConversion) and not isinstance(target, DictConversion):
110
+ return
111
+
112
+ for attr_name, attr_value in source.__dict__.items():
113
+ setattr(target, attr_name, attr_value)
114
+
115
+
116
+ def is_hovered(x=None, y=None, width=5, height=None):
117
+ padding = imgui.get_style().window_padding
118
+ if x is None:
119
+ x = imgui.get_cursor_screen_pos()[0] - padding[0]
120
+ if y is None:
121
+ y = imgui.get_cursor_screen_pos()[1] - padding[1]
122
+ if height is None:
123
+ line_height = imgui.get_text_line_height()
124
+ height = line_height + padding[1] * 2
125
+ return imgui.is_mouse_hovering_rect(x, y, x + max(width, 10), y + height)
126
+
127
+
128
+ def render_dict_as_table(input_value=None, config=None):
129
+ unique = config.unique
130
+
131
+ num_columns = 2 # For key / value pair
132
+ imgui.columns(num_columns, f"dict_columns##{unique}", True)
133
+ for key, value in input_value.items():
134
+ imgui.text(f"{key}")
135
+ imgui.next_column()
136
+ imgui.text(f"{value}")
137
+ imgui.next_column()
138
+
139
+ imgui.columns(1) # Reset to single column layout
140
+
141
+
142
+ def draw_rect(x=None, y=None, width=5, height=None, color=(1, 1, 1, 1)):
143
+ padding = imgui.get_style().window_padding
144
+ if x is None:
145
+ x = imgui.get_cursor_screen_pos()[0] - padding[0]
146
+ if y is None:
147
+ y = imgui.get_cursor_screen_pos()[1] - padding[1]
148
+ if height is None:
149
+ line_height = imgui.get_text_line_height()
150
+ height = line_height + padding[1] * 2
151
+ imgui.get_foreground_draw_list().add_rect_filled(
152
+ x, y, x + width, y + height, pack_color(*color))
153
+
154
+ clicked = False
155
+ if imgui.is_mouse_hovering_rect(x, y, x + max(width, 10), y + height):
156
+ white = (1.0, 1.0, 1.0, 1.0)
157
+ imgui.get_foreground_draw_list().add_rect(
158
+ x, y, x + width, y + height, pack_color(*white))
159
+ if imgui.is_mouse_clicked(imgui.MOUSE_BUTTON_LEFT):
160
+ clicked = True
161
+ return clicked
@@ -0,0 +1,278 @@
1
+ """Dropdown core functions and supporting definitions."""
2
+ from meltygui.core.runtime.toggles import Toggles
3
+ from meltygui.core.melty import Melty
4
+ from meltygui.core.rendering.core_decoration import Core
5
+
6
+
7
+ def _close_menu(draw_state, state):
8
+ """Close whatever menu is showing: release the popover slot, collapse
9
+ that menu's paths, hand text focus back to whoever had it."""
10
+ from meltygui.core.windowing.glfw_utils import request_render
11
+
12
+ if state.open_title is not None:
13
+ menu_state = state.menus.get(state.open_title)
14
+ _dd_close(menu_state)
15
+ # A picked action may remove this state before its next draw call.
16
+ # Close its surface now rather than waiting for closed=True next frame.
17
+ if menu_state is not None and menu_state._menu_ds is not None:
18
+ menu_state._menu_ds.closed = True
19
+ state.open_title = None
20
+ if Melty.popover_focused_ds is draw_state:
21
+ Melty.popover_focused_ds = None
22
+ if Melty.text_focused_ds is draw_state:
23
+ previous = state._prev_text_focus
24
+ Melty.text_focused_ds = previous if previous is not draw_state else None
25
+ if previous is not None:
26
+ Melty._text_focus_grant_frame = Melty.frame_count
27
+ state._prev_text_focus = None
28
+ draw_state.invalidate()
29
+ request_render()
30
+
31
+
32
+ def _ds_in_subtree(node, ancestor, max_depth=64):
33
+ """Use the same window ancestry as focus dispatch, including popovers."""
34
+ return ancestor.id in Core.melty.ancestor_closure([node])
35
+
36
+
37
+ def _dd_set_cursor(root_state, cursor_path, is_branch, menu_ds=None):
38
+ """Point the highlight at `cursor_path` and derive the open path from it: a
39
+ branch expands its own sub-menu, a leaf collapses back to its parent level.
40
+ This is the single writer for both hover and keyboard, so they stay in sync.
41
+
42
+ When the OPEN path changes, the containing menu's row tiles must re-run:
43
+ a branch row stamps `closed` on its submenu window only when its body
44
+ actually runs, and the blit cache has no kwargs key — a clean row tile
45
+ blit-skips even though its open_path input changed, leaving the submenu
46
+ stamped open forever (one leaked submenu per branch row swept). Keyboard
47
+ nav never leaked because begin_frame cascades invalidate_up from the menu
48
+ tile on every nav key; this is the hover-side equivalent. `menu_ds` is the
49
+ level's menu-window draw_state (row tiles are its direct tile children)."""
50
+ from meltygui.model.dropdown_model import _dd_as_tuple
51
+
52
+ if root_state is None:
53
+ return
54
+ new_cursor = tuple(cursor_path)
55
+ new_open = tuple(cursor_path) if is_branch else tuple(cursor_path[:-1])
56
+ old_open = _dd_as_tuple(root_state.open_path)
57
+ if (new_cursor == _dd_as_tuple(root_state.cursor_path)
58
+ and new_open == old_open):
59
+ return
60
+ root_state.cursor_path = new_cursor
61
+ root_state.open_path = new_open
62
+ if new_open != old_open:
63
+ _dd_invalidate_rows(root_state, menu_ds)
64
+
65
+
66
+ def _dd_invalidate_rows(root_state, menu_ds=None):
67
+ """Cascade-invalidate a menu window's tile subtree so every row body
68
+ re-runs and re-stamps `closed` on its submenu from the current open_path.
69
+ Without a menu_ds at hand (close paths), resolve the root menu through the
70
+ search box tile — its parent_window is the root menu. bypass_clip so a
71
+ branch row scrolled out of the menu viewport is cleaned too (otherwise
72
+ scrolling it back in would revive its stale-open submenu). Submenu windows
73
+ are tile ROOTS (not tile children of their spawner rows), so this reaches
74
+ rows, not window interiors — which is all closing needs."""
75
+ from meltygui.core.windowing.glfw_utils import request_render
76
+
77
+ if menu_ds is None and root_state is not None:
78
+ box_tile = getattr(root_state, "_search_box_tile", None)
79
+ if box_tile is not None:
80
+ box_ds = Melty.cache.key_to_draw_state.get(box_tile)
81
+ menu_ds = getattr(box_ds, "parent_window", None) if box_ds is not None else None
82
+ if menu_ds is None or getattr(menu_ds, "_tile_id", None) is None:
83
+ return
84
+ from meltygui.core.cache.invalidation_tracker import Note
85
+ Melty.cache.invalidate_up(menu_ds._tile_id, force=True, bypass_clip=True,
86
+ note=Note(name="dd open_path change", tint=(1, 0.6, 0.2)))
87
+ request_render()
88
+
89
+
90
+ def _dd_scroll_cursor_into_view(menu_ds, row_index, row0_offset=0.0, pitch=None):
91
+ """Nudge a menu window's scroll_offset the minimal amount so keyboard-cursor
92
+ row `row_index` is fully visible. Rows are a fixed _DD_ROW_H pitch drawn
93
+ flush from the content origin in the manual-loop path; `row0_offset` covers
94
+ a level that draws chrome above its rows. Stateless — pure geometry from
95
+ the live draw_state, clamped to the wrapper-published _max_scroll_y so this
96
+ writer never fights core_render's own clamp."""
97
+ from meltygui.core.windowing.glfw_utils import request_render
98
+
99
+ if menu_ds is None or row_index is None or row_index < 0:
100
+ return
101
+ pitch = Toggles.Dropdown.row_height if pitch is None else pitch # the usage picker's rows are taller
102
+ view_h = menu_ds.abs_clipped_height - menu_ds.header_height - menu_ds.footer_height
103
+ sx, sy = menu_ds.scroll_offset
104
+ row_top = row0_offset + row_index * pitch
105
+ row_bot = row_top + pitch
106
+ new_sy = sy
107
+ if view_h > 0 and row_bot > new_sy + view_h: # below the viewport: minimal scroll down
108
+ new_sy = row_bot - view_h
109
+ max_y = menu_ds._max_scroll_y
110
+ if max_y is not None:
111
+ new_sy = min(new_sy, max_y)
112
+ if row_top < new_sy: # above the viewport (top-centers a tiny view)
113
+ new_sy = row_top
114
+ new_sy = max(0.0, new_sy)
115
+ if new_sy != sy:
116
+ menu_ds.scroll_offset = (sx, new_sy)
117
+ # Own the repaint edge: scroll is BAKED into the blit capture at render
118
+ # time and only the wheel-event callback repaints on scroll change, so a
119
+ # clean tile would replay the old-offset capture (the caller's repaint gate
120
+ # doesn't always fire - e.g. a snap-to-top when the filtered list is
121
+ # value-identical). Change-edge-gated by `new_sy != sy` above, never
122
+ # per-frame. bypass_clip: the popup can protrude outside its parent.
123
+ if menu_ds._tile_id is not None:
124
+ from meltygui.core.cache.invalidation_tracker import Note
125
+ Melty.cache.invalidate_up(menu_ds._tile_id, force=True, bypass_clip=True,
126
+ note=Note(name="dd scroll-into-view", tint=(1, 0.6, 0.2)))
127
+ request_render()
128
+
129
+
130
+ def _dd_pick(root_state, path):
131
+ """Keep a selection until the owning dropdown consumes it.
132
+
133
+ Submenus render as independent floating windows, often after their parent
134
+ has already drawn. A one-frame return value cannot reliably cross that
135
+ boundary; the shared dropdown state owns the pending selection.
136
+ """
137
+ from meltygui.core.windowing.glfw_utils import request_render
138
+
139
+ root_state._picked_path = tuple(path)
140
+ root_state._pending_pick = tuple(path)
141
+ owner = Melty.popover_focused_ds
142
+ if owner is not None:
143
+ owner.invalidate_up()
144
+ request_render()
145
+
146
+
147
+ def _dd_close(root_state):
148
+ """Reset popover state on close: collapse the open/cursor paths, clear the
149
+ search query, and release the search box's text focus if it held it."""
150
+ from meltygui.model.dropdown_model import _dd_as_tuple
151
+
152
+ if root_state is None:
153
+ return
154
+ menu = getattr(root_state, "_menu_ds", None)
155
+ if menu is not None:
156
+ menu.closed = True
157
+ root_state._pending_pick = None
158
+ open_was = _dd_as_tuple(root_state.open_path)
159
+ root_state.open_path = ()
160
+ root_state.cursor_path = ()
161
+ root_state.search_query = ""
162
+ root_state.search = ""
163
+ root_state._focus_search = 0
164
+ root_state._had_focus = False
165
+ # A branch row left stamped open would blit-skip on reopen and revive its
166
+ # submenu window even though open_path was reset - dirty the rows now (the
167
+ # flags persist while the menu is hidden) so the first reopen render
168
+ # re-stamps every submenu closed.
169
+ if open_was:
170
+ _dd_invalidate_rows(root_state)
171
+ box_tile = getattr(root_state, "_search_box_tile", None)
172
+ tf = Melty.text_focused_ds
173
+ if tf is not None and box_tile is not None and getattr(tf, "_tile_id", None) == box_tile:
174
+ Melty.text_focused_ds = None
175
+
176
+
177
+ def _dd_handle_keys(collection, root_state, search="", text_focused=False):
178
+ """Arrow-key navigation while the popover is open. Up/Down move within the
179
+ current level, Right (or Enter on a branch) descends, Left collapses to the
180
+ parent, Enter on a leaf picks it. Returns the picked leaf value, or
181
+ UNSET_VALUE when nothing was chosen this frame. Reads the GLFW-callback key
182
+ queue so it works without the menu being hovered."""
183
+ from meltygui.core.conversion.cache_tree import UNSET_VALUE
184
+ from meltygui.model.dropdown_model import _dd_as_tuple
185
+ from meltygui.model.dropdown_model import _dd_rows_at
186
+ from meltygui.core.windowing.glfw_utils import request_render
187
+ import meltygui.core.windowing.window_api as glfw
188
+
189
+ if root_state is None:
190
+ return UNSET_VALUE
191
+ keys = list(Core.melty.frame_key_events)
192
+
193
+ def pressed(*codes):
194
+ return any(k in codes for k, _ in keys)
195
+
196
+ down = pressed(glfw.KEY_DOWN)
197
+ up = pressed(glfw.KEY_UP)
198
+ right = pressed(glfw.KEY_RIGHT)
199
+ left = pressed(glfw.KEY_LEFT)
200
+ enter = pressed(glfw.KEY_ENTER, glfw.KEY_KP_ENTER)
201
+ # While typing a query, Left/Right are the search box's text cursor, not tree
202
+ # navigation, so the dropdown doesn't double-act on them.
203
+ if text_focused and search:
204
+ left = right = False
205
+ if not (down or up or right or left or enter):
206
+ return UNSET_VALUE
207
+
208
+ # A nav key fired: switch to keyboard-select mode so hover stops moving the
209
+ # cursor until the mouse actually moves again (cleared in draw_dropdown).
210
+ root_state._kbd_mode = True
211
+
212
+ # Navigation level = the cursor's parent; rows = its (search-filtered)
213
+ # siblings. Fall back to the root level if the cursor path went stale.
214
+ cursor = _dd_as_tuple(getattr(root_state, "cursor_path", ()))
215
+ level = cursor[:-1]
216
+ rows = _dd_rows_at(collection, level, search)
217
+ if not rows:
218
+ level = ()
219
+ rows = _dd_rows_at(collection, level, search)
220
+ cursor = ()
221
+ if not rows:
222
+ return UNSET_VALUE
223
+
224
+ level_keys = [r[0] for r in rows]
225
+ had_cursor = bool(cursor) and cursor[-1] in level_keys
226
+ idx = level_keys.index(cursor[-1]) if had_cursor else 0
227
+
228
+ # Left collapses the current sub-menu and highlights its parent row.
229
+ if left and level:
230
+ root_state.cursor_path = tuple(level)
231
+ root_state.open_path = tuple(level[:-1])
232
+ request_render()
233
+ return UNSET_VALUE
234
+
235
+ # From no selection, the first Up/Down just focuses on the first row; otherwise
236
+ # it steps (wrapping). Right/Enter act on whatever row is current.
237
+ if had_cursor:
238
+ if down:
239
+ idx = (idx + 1) % len(rows)
240
+ elif up:
241
+ idx = (idx - 1) % len(rows)
242
+ key, value, label, is_branch = rows[idx]
243
+ new_cursor = tuple(level) + (key,)
244
+
245
+ # Right / Enter on a branch descends into its first visible child.
246
+ if (right or enter) and is_branch:
247
+ kids = _dd_rows_at(collection, new_cursor, search)
248
+ if kids:
249
+ ck, cv, _cl, cbr = kids[0]
250
+ _dd_set_cursor(root_state, new_cursor + (ck,), cbr)
251
+ request_render()
252
+ return UNSET_VALUE
253
+
254
+ if enter and not is_branch:
255
+ _dd_set_cursor(root_state, new_cursor, False)
256
+ root_state._picked_path = tuple(new_cursor)
257
+ return value
258
+
259
+ _dd_set_cursor(root_state, new_cursor, is_branch)
260
+ request_render()
261
+ return UNSET_VALUE
262
+
263
+
264
+ def _dd_update_menu_size(state, menu_ds, min_width=None, max_height=None):
265
+ from meltygui.core.windowing.glfw_utils import request_render
266
+ from meltygui.view.dropdown_view import _dd_menu_fit
267
+
268
+ current = (menu_ds.width, menu_ds.height)
269
+ last_fit = getattr(state, "_menu_fit", None)
270
+ if (getattr(menu_ds, "_initial_window_size", None) is not None
271
+ and last_fit is not None and current != last_fit and all(current)):
272
+ state.menu_size = current
273
+ elif getattr(state, "menu_size", None) is None:
274
+ fit = _dd_menu_fit(menu_ds, min_width=min_width, max_height=max_height)
275
+ if fit is not None and fit != current:
276
+ menu_ds.width, menu_ds.height = fit
277
+ request_render()
278
+ state._menu_fit = (menu_ds.width, menu_ds.height)
@@ -0,0 +1,155 @@
1
+ """One-dimensional edge constraints, independent of views and native backends.
2
+
3
+ Cells connect shared edge objects with a minimum and optional maximum span.
4
+ Motion propagates only through those connections. The caller supplies walls and
5
+ chooses which contacts belong to an interaction; overlapping coordinates alone
6
+ do not create a collision.
7
+ """
8
+
9
+ class EdgeGraph:
10
+ """Adjacency over cells: ``ahead[id(near)]`` → ``[(far, floor, cap)]``,
11
+ ``behind[id(far)]`` → ``[(near, floor, cap)]``, ``nodes`` id → edge."""
12
+
13
+ def __init__(self, cells):
14
+ self.ahead, self.behind, self.nodes = {}, {}, {}
15
+ for near, far, floor, cap in cells:
16
+ self.nodes[id(near)] = near
17
+ self.nodes[id(far)] = far
18
+ self.ahead.setdefault(id(near), []).append((far, floor, cap))
19
+ self.behind.setdefault(id(far), []).append((near, floor, cap))
20
+
21
+ def chain(self, start, goal, walls=frozenset(), forward=True,
22
+ capped_only=False):
23
+ """Weight of the binding chain of cells from ``start`` to ``goal``:
24
+ the LONGEST sum of floors (a push chain — ``capped_only=False``) or
25
+ the SHORTEST sum of caps over capped cells only (a pull chain —
26
+ ``capped_only=True``), walking cells ahead (``forward``) or behind.
27
+ None when no chain links them. Chains never pass through another
28
+ wall: a wall never moves, so nothing propagates past it."""
29
+ links = self.ahead if forward else self.behind
30
+ pick = min if capped_only else max
31
+ memo, on_stack = {}, set()
32
+
33
+ def best(edge):
34
+ edge_id = id(edge)
35
+ if edge_id == id(goal):
36
+ return 0.0
37
+ if edge_id in walls or edge_id in on_stack:
38
+ return None
39
+ if edge_id in memo:
40
+ return memo[edge_id]
41
+ on_stack.add(edge_id)
42
+ found = None
43
+ for other, floor, cap in links.get(edge_id, ()):
44
+ if capped_only:
45
+ if cap is None:
46
+ continue
47
+ weight = cap
48
+ else:
49
+ weight = floor
50
+ rest = best(other)
51
+ if rest is None:
52
+ continue
53
+ total = weight + rest
54
+ found = total if found is None else pick(found, total)
55
+ on_stack.discard(edge_id)
56
+ memo[edge_id] = found
57
+ return found
58
+
59
+ return best(start)
60
+
61
+
62
+ def solve_edge(graph, edge, target, walls=frozenset(), axis="x"):
63
+ """Move ``edge`` to ``target`` through the cell graph. Edges are
64
+ independent objects: no other edge moves unless the moving edge (or
65
+ one it already carried) makes CONTACT through a cell — two kinds, one
66
+ per side of the moving edge:
67
+
68
+ PUSH, ahead: the cell in front closes to its floor (_edge_min) and
69
+ its far edge is shoved on ahead.
70
+ PULL, behind: the cell it leaves behind opens to its cap (_edge_max)
71
+ and its far edge is dragged along behind.
72
+
73
+ Either chain runs cell by cell — a pushed edge closes the next cell, a
74
+ pulled edge opens the next — and stops at the first cell with slack;
75
+ consecutive capped cells therefore travel as one, exactly as
76
+ consecutive floor-packed cells do. A cell with no cap never pulls. An
77
+ edge several cells share (a nested layout's far edge) carries every
78
+ cell it bounds.
79
+
80
+ ``walls`` is a set of edge ids the cascade must NOT move. Contact stops
81
+ dead at a wall: the *dragged* edge itself is clamped so the pile packs
82
+ against the wall at its floors (push side) or stretches to its summed
83
+ caps (pull side) instead of the chain shoving the wall along. Used by
84
+ _solve_collisions to keep one FRAME edge from moving the other (breaks
85
+ the foreign-width feedback loop — see there; its mirror image is a
86
+ fully-capped row, which refuses a foreign widening the same way);
87
+ interior divider drags pass no walls, so a divider can still push or
88
+ pull a frame edge and slide/grow/shrink the window 1:1 with the
89
+ cursor. Returns True if anything moved."""
90
+ old = edge[axis]
91
+ if target == old or id(edge) not in graph.nodes:
92
+ return False
93
+ forward = target > old
94
+ # Wall clamps first: ahead through the floors, behind through the caps
95
+ # (a pull chain can only reach a wall over capped cells). The
96
+ # binding chain per wall is the longest floor chain / shortest cap
97
+ # chain - the one that would move the wall first.
98
+ for wall_id in walls:
99
+ wall = graph.nodes.get(wall_id)
100
+ if wall is None or wall is edge:
101
+ continue
102
+ push = graph.chain(edge, wall, walls, forward=forward)
103
+ if push is not None:
104
+ target = (min(target, wall[axis] - push) if forward
105
+ else max(target, wall[axis] + push))
106
+ pull = graph.chain(edge, wall, walls, forward=not forward,
107
+ capped_only=True)
108
+ if pull is not None:
109
+ target = (min(target, wall[axis] + pull) if forward
110
+ else max(target, wall[axis] - pull))
111
+ if target == old:
112
+ return False
113
+ edge[axis] = float(target)
114
+ # Propagate contact. Every relaxation moves an edge the way the drag
115
+ # went and never back, so this is a monotone worklist that settles on
116
+ # its own; the guard only ever trips on a cyclic (corrupt) cell graph.
117
+ pending = [edge]
118
+ guard = 64 * (len(graph.nodes) + 1)
119
+ while pending and guard > 0:
120
+ guard -= 1
121
+ current = pending.pop()
122
+ current_id = id(current)
123
+ if forward:
124
+ for far, floor, _cap in graph.ahead.get(current_id, ()): # push ahead
125
+ if id(far) in walls:
126
+ continue
127
+ need = current[axis] + floor
128
+ if far[axis] < need:
129
+ far[axis] = need
130
+ pending.append(far)
131
+ for near, _floor, cap in graph.behind.get(current_id, ()): # pull behind
132
+ if cap is None or id(near) in walls:
133
+ continue
134
+ need = current[axis] - cap
135
+ if near[axis] < need:
136
+ near[axis] = need
137
+ pending.append(near)
138
+ else:
139
+ for near, floor, _cap in graph.behind.get(current_id, ()): # push ahead
140
+ if id(near) in walls:
141
+ continue
142
+ need = current[axis] - floor
143
+ if near[axis] > need:
144
+ near[axis] = need
145
+ pending.append(near)
146
+ for far, _floor, cap in graph.ahead.get(current_id, ()): # pull behind
147
+ if cap is None or id(far) in walls:
148
+ continue
149
+ need = current[axis] + cap
150
+ if far[axis] > need:
151
+ far[axis] = need
152
+ pending.append(far)
153
+ return True
154
+
155
+