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,41 @@
1
+ """Collection model functions and supporting definitions."""
2
+ from enum import Enum
3
+ from meltygui.core.runtime.toggles import Toggles
4
+ from types import NoneType
5
+
6
+
7
+ def annotation_item_type(annotation):
8
+ """Item type a collection annotation implies for new entries:
9
+ Dict[str, Lora] -> Lora, List[X] -> X, Optional[T] -> T. None when the
10
+ annotation carries no usable element type."""
11
+ args = [a for a in getattr(annotation, "__args__", ()) if a is not NoneType]
12
+ if not args:
13
+ return None
14
+ return args[-1]
15
+
16
+
17
+ def _collection_match_keys(input_value, keys, excluded, show_excluded):
18
+ """The (index, lowercased key string) pairs draw_collection renders and
19
+ searches, in key order — the basis for both counting key matches and
20
+ resolving which key holds the current match, without rendering. `index` is
21
+ the position in `keys`, so it lines up with the render loop. Mirrors the
22
+ loop's key-string derivation and skip filters."""
23
+ out = []
24
+ parent_cls_name = input_value.__class__.__name__
25
+ excl_attrs = getattr(type(input_value), "__excluded_attrs__", None)
26
+ for idx, key in enumerate(keys):
27
+ if isinstance(key, (float, Enum, NoneType)):
28
+ key_str = parent_cls_name
29
+ elif isinstance(key, int):
30
+ key_str = f"{key}"
31
+ else:
32
+ key_str = str(key)
33
+ if str(key).split("##")[0] in excluded:
34
+ continue
35
+ if (not show_excluded and excl_attrs is not None
36
+ and not Toggles.show_excluded and str(key) in excl_attrs):
37
+ continue
38
+ if not show_excluded and (key_str.startswith("_") or key_str.endswith("_")):
39
+ continue
40
+ out.append((idx, key_str.lower()))
41
+ return out
@@ -0,0 +1,127 @@
1
+ """Color model functions and supporting definitions."""
2
+ from meltygui.hdr_color import scale_saturation
3
+
4
+
5
+ def _brightness_clamp(r, g, b, min_b, max_b):
6
+ """Clamp PERCEIVED brightness (0.299r + 0.587g + 0.114b) — the
7
+ legibility guard for tinted colors. Both directions SCALE the channels,
8
+ which preserves their ratios and therefore saturation — a dark
9
+ high-saturation tint lifts to a dark high-saturation color, it does NOT
10
+ wash toward gray (a uniform add did, which made lowering the value
11
+ factor also lose saturation). Only true near-black — no hue left to
12
+ preserve — falls back to the uniform add."""
13
+ # Inverted clamp (min above max - mid-drag or experimental toggle values)
14
+ # collapses to the floor: without this, dark colors LIFT to min while
15
+ # bright ones CRUSH to max < min, inverting brightness ordering and pinning
16
+ # every wash to near-identical luminance ("tints stopped responding").
17
+ if 0 < max_b < min_b:
18
+ max_b = min_b
19
+ lum = 0.299 * r + 0.587 * g + 0.114 * b
20
+ if lum < min_b:
21
+ if lum > 1e-4:
22
+ k = min_b / lum
23
+ return min(1.0, r * k), min(1.0, g * k), min(1.0, b * k)
24
+ d = min_b - lum
25
+ return min(1.0, r + d), min(1.0, g + d), min(1.0, b + d)
26
+ if lum > max_b > 0 and lum > 0:
27
+ k = max_b / lum
28
+ return r * k, g * k, b * k
29
+ return r, g, b
30
+
31
+
32
+ def _clamp_bg_value(color, max_bg_value):
33
+ """Cap a background color's VALUE (max channel, as in HSV) at `max_bg_value`,
34
+ keeping hue exact and BOOSTING saturation by the same factor the value was
35
+ cut by (s / k, clamped to 1.0). A plain uniform channel scale holds HSV
36
+ saturation constant but still reads as washed out once it's dark, so the
37
+ boost buys the colorfulness back — the color only ever gets darker and
38
+ *more* saturated, never grayer.
39
+
40
+ This is the LAST thing applied to a bg color — it bounds the color actually
41
+ painted, not the depth ramp that fed it, so whatever the depth/tint/bleed
42
+ chain produced, `max_bg_value=0` is black and `0.2` is at most 20% value.
43
+ Deliberately not text_editor's _brightness_clamp: that one is a perceptual
44
+ (luma) guard and no-ops at max_b == 0, which would break the black case.
45
+
46
+ Done in raw channel arithmetic rather than a colorsys round trip: hue is
47
+ just the position of the mid channel in the [min, max] span, so rebuilding
48
+ against the new chroma preserves it without ever naming an angle."""
49
+ if max_bg_value is None or color is None or len(color) < 3:
50
+ return color
51
+ rest = tuple(color[3:])
52
+ red, green, blue = color[0], color[1], color[2]
53
+ value = max(red, green, blue)
54
+ if value <= max_bg_value:
55
+ return color
56
+ if max_bg_value <= 0 or value <= 0:
57
+ return (0.0, 0.0, 0.0) + rest
58
+
59
+ low = min(red, green, blue)
60
+ if low >= value: # achromatic - no hue to preserve, just darken
61
+ return (max_bg_value, max_bg_value, max_bg_value) + rest
62
+
63
+ # k is the cut applied to the value; undo it with saturation.
64
+ k = max_bg_value / value
65
+ saturation = scale_saturation((value - low) / value, 1.0 / k)
66
+ chroma = max_bg_value * saturation
67
+ new_low = max_bg_value - chroma
68
+ span = value - low
69
+ return (new_low + (red - low) / span * chroma,
70
+ new_low + (green - low) / span * chroma,
71
+ new_low + (blue - low) / span * chroma) + rest
72
+
73
+
74
+ def _wide_pick(fx, fy, top_fraction, max_stops):
75
+ """Square fractions (x right, y down) → (s, v, exposure)."""
76
+ fx = min(max(fx, 0.0), 1.0)
77
+ fy = min(max(fy, 0.0), 1.0)
78
+ if fy < top_fraction:
79
+ return fx, 1.0, 2.0 ** (max_stops * (1.0 - fy / top_fraction))
80
+ v = 1.0 - (fy - top_fraction) / max(1e-6, 1.0 - top_fraction)
81
+ return fx, min(max(v, 0.0), 1.0), 1.0
82
+
83
+
84
+ def _wide_marker(s, v, exposure, top_fraction, max_stops):
85
+ """(s, v, exposure) → square fractions; the inverse of _wide_pick."""
86
+ import math
87
+ if exposure > 1.0:
88
+ fy = top_fraction * (1.0 - min(1.0, math.log2(exposure) / max_stops))
89
+ else:
90
+ fy = top_fraction + (1.0 - v) * (1.0 - top_fraction)
91
+ return min(max(s, 0.0), 1.0), min(max(fy, 0.0), 1.0)
92
+
93
+
94
+ def _srgb_plus_pick(px, py, square, ext, band, max_stops):
95
+ """Cursor offset from the SQUARE's top-left, in px (negative y = over
96
+ the exposure band, x past `square` = over the P3 strip) →
97
+ (s, x, v, exposure)."""
98
+ if px <= square:
99
+ s, x = max(px / square, 0.0), 0.0
100
+ else:
101
+ s, x = 1.0, min(max((px - square) / max(1e-6, ext), 0.0), 1.0)
102
+ if py < 0.0:
103
+ fy = min(max(-py / max(1e-6, band), 0.0), 1.0) # 0 at the seam, 1 at the top
104
+ return s, x, 1.0, 2.0 ** (max_stops * fy)
105
+ v = 1.0 - min(max(py / square, 0.0), 1.0)
106
+ return s, x, v, 1.0
107
+
108
+
109
+ def _srgb_plus_marker(s, x, v, exposure, square, ext, band, max_stops):
110
+ """(s, x, v, exposure) → marker offset from the square's top-left, in
111
+ px; the inverse of _srgb_plus_pick."""
112
+ import math
113
+ px = s * square if x <= 0.0 else square + x * ext
114
+ if exposure > 1.0:
115
+ py = -band * min(1.0, math.log2(exposure) / max_stops)
116
+ else:
117
+ py = (1.0 - v) * square
118
+ return px, py
119
+
120
+
121
+ def _extension_pick(fx, ext_fraction):
122
+ """Cursor x as a fraction of the SQUARE's width (past 1 = over the
123
+ extension, whose width is ext_fraction squares) → (s, x): the classic
124
+ saturation inside the square, s = 1 and the P3 depth x past the seam."""
125
+ if fx <= 1.0:
126
+ return max(fx, 0.0), 0.0
127
+ return 1.0, min(max((fx - 1.0) / max(1e-6, ext_fraction), 0.0), 1.0)
@@ -0,0 +1,36 @@
1
+ """CUDA decoding of tensor primitive dtypes without packing source storage."""
2
+
3
+ DTYPE_CODES = {
4
+ "torch.float32": 0, "torch.float16": 1, "torch.bfloat16": 2,
5
+ "torch.float64": 3, "torch.int8": 4, "torch.uint8": 5, "torch.bool": 5,
6
+ "torch.int16": 6, "torch.int32": 7, "torch.int64": 8,
7
+ }
8
+
9
+ CUDA_LOAD_SOURCE = r"""
10
+ #include <cuda_fp16.h>
11
+
12
+ __device__ __forceinline__ float load_at(const unsigned char* __restrict__ d,
13
+ int dtype, long long e) {
14
+ switch (dtype) {
15
+ case 0: return ((const float*)d)[e];
16
+ case 1: return __half2float(((const __half*)d)[e]);
17
+ case 2: { unsigned short u = ((const unsigned short*)d)[e];
18
+ return __uint_as_float(((unsigned)u) << 16); }
19
+ case 3: return (float)((const double*)d)[e];
20
+ case 4: return (float)((const signed char*)d)[e];
21
+ case 5: return (float)d[e];
22
+ case 6: return (float)((const short*)d)[e];
23
+ case 7: return (float)((const int*)d)[e];
24
+ case 8: return (float)((const long long*)d)[e];
25
+ }
26
+ return 0.0f;
27
+ }
28
+
29
+ """
30
+
31
+
32
+ def dtype_code(t):
33
+ code = DTYPE_CODES.get(str(t.dtype))
34
+ if code is None:
35
+ raise ValueError(f"unsupported CUDA tensor dtype {t.dtype}")
36
+ return code
@@ -0,0 +1,149 @@
1
+ """Versioned CUDA tensors exposed as GLTexture values without a CPU upload.
2
+
3
+ The injected GLState owns each texture/PBO/registration as one resource. CUDA
4
+ unregistration must finish before the GL buffer is deleted. The resource holds
5
+ no reference to the source tensor; cache hits do no tensor copies or syncs.
6
+ """
7
+ import ctypes
8
+
9
+ import OpenGL.GL as gl
10
+
11
+ from meltygui.core.graphics.cuda_context_core import current_device_index
12
+ from meltygui.core.graphics.cuda_interop_core import (
13
+ interop_context, copy_to_buffer, log_once,
14
+ register_buffer, unregister_buffer,
15
+ )
16
+ from meltygui.core.graphics.gl_state import (
17
+ GLTexture, _scalar, current_context, is_gl_thread, texture3d_fit, tight_unpack,
18
+ )
19
+
20
+
21
+ class CudaVolume:
22
+ """Owned allocation behind a texture value; its source tensor stays external."""
23
+ __slots__ = ("texture", "buffer", "registered", "nbytes", "last_version")
24
+
25
+ def __init__(self, texture, buffer, registered, nbytes):
26
+ self.texture = texture
27
+ self.buffer = buffer
28
+ self.registered = registered
29
+ self.nbytes = nbytes
30
+ self.last_version = object() # even version=None must perform its first upload
31
+
32
+
33
+ def _release_volume(volume, context):
34
+ # Each completed step is recorded so a failed deletion can be retried.
35
+ # Keep the implementation at module scope so queued deleters see hot edits.
36
+ if volume.registered is not None:
37
+ unregister_buffer(volume.registered, context)
38
+ volume.registered = None
39
+ if volume.buffer is not None:
40
+ gl.glDeleteBuffers(1, [volume.buffer])
41
+ volume.buffer = None
42
+ if volume.texture is not None:
43
+ gl.glDeleteTextures([volume.texture.texture_id])
44
+ volume.texture = None
45
+
46
+
47
+ def tensor_to_texture(gl_state, key, tensor, version):
48
+ """Return a versioned 3-D float16/32 GLTexture, or None for CPU fallback.
49
+
50
+ Noncontiguous tensors are packed only on version misses. Tensors on another
51
+ GPU are staged by Torch onto the GL device, never copied across devices with
52
+ a raw CUDA pointer copy. Calls require the owner's GL context and a compatible
53
+ recorded CUDA context. Core activates it locally and restores the caller's
54
+ native context, or lazily establishes the display context when none exists.
55
+ An incompatible caller-owned PyCUDA context is left untouched.
56
+ """
57
+ if not is_gl_thread() or current_context() is None:
58
+ return None
59
+ if gl_state._context != current_context():
60
+ return None
61
+ import torch
62
+ if not (isinstance(tensor, torch.Tensor) and tensor.is_cuda
63
+ and tensor.dim() == 3 and tensor.dtype in (torch.float16, torch.float32)):
64
+ return None
65
+ with interop_context() as context:
66
+ if context is None:
67
+ return None
68
+ return _upload_tensor(gl_state, key, tensor, version, context)
69
+
70
+
71
+ def _upload_tensor(gl_state, key, tensor, version, context):
72
+ import torch
73
+ device = current_device_index()
74
+ depth, height, width = (int(size) for size in tensor.shape)
75
+ half = tensor.dtype == torch.float16
76
+ internal = gl.GL_R16F if half else gl.GL_R32F
77
+ gl_type = gl.GL_HALF_FLOAT if half else gl.GL_FLOAT
78
+ nbytes = tensor.nelement() * tensor.element_size()
79
+
80
+ def delete(volume):
81
+ _release_volume(volume, context)
82
+
83
+ def create():
84
+ _, problems = texture3d_fit((depth, height, width), tensor.element_size())
85
+ if problems:
86
+ raise ValueError("tensor_to_texture: " + "; ".join(problems))
87
+ volume = CudaVolume(None, None, None, nbytes)
88
+ previous_buffer = _scalar(gl.glGetIntegerv(gl.GL_ARRAY_BUFFER_BINDING))
89
+ previous_unpack = _scalar(gl.glGetIntegerv(gl.GL_PIXEL_UNPACK_BUFFER_BINDING))
90
+ previous_texture = _scalar(gl.glGetIntegerv(gl.GL_TEXTURE_BINDING_3D))
91
+ try:
92
+ volume.buffer = _scalar(gl.glGenBuffers(1))
93
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, volume.buffer)
94
+ gl.glBufferData(gl.GL_ARRAY_BUFFER, nbytes, None, gl.GL_DYNAMIC_DRAW)
95
+ volume.registered = register_buffer(volume.buffer)
96
+ texture_id = _scalar(gl.glGenTextures(1))
97
+ volume.texture = GLTexture(texture_id, gl.GL_TEXTURE_3D,
98
+ (depth, height, width), internal)
99
+ gl.glBindTexture(gl.GL_TEXTURE_3D, texture_id)
100
+ # NULL must mean no client data, not offset zero in a caller's PBO.
101
+ gl.glBindBuffer(gl.GL_PIXEL_UNPACK_BUFFER, 0)
102
+ gl.glTexImage3D(gl.GL_TEXTURE_3D, 0, internal, width, height, depth, 0,
103
+ gl.GL_RED, gl_type, ctypes.c_void_p(0))
104
+ for parameter, value in (
105
+ (gl.GL_TEXTURE_MIN_FILTER, gl.GL_NEAREST),
106
+ (gl.GL_TEXTURE_MAG_FILTER, gl.GL_NEAREST),
107
+ (gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE),
108
+ (gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE),
109
+ (gl.GL_TEXTURE_WRAP_R, gl.GL_CLAMP_TO_EDGE)):
110
+ gl.glTexParameteri(gl.GL_TEXTURE_3D, parameter, value)
111
+ return volume
112
+ except Exception:
113
+ gl_state.defer_delete(key, volume, delete)
114
+ raise
115
+ finally:
116
+ gl.glBindTexture(gl.GL_TEXTURE_3D, previous_texture)
117
+ gl.glBindBuffer(gl.GL_PIXEL_UNPACK_BUFFER, previous_unpack)
118
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, previous_buffer)
119
+
120
+ try:
121
+ volume = gl_state.get(key, create, delete,
122
+ deps=((depth, height, width), "f16" if half else "f32", context))
123
+ if volume.last_version != version:
124
+ if tensor.device.index != device:
125
+ # Finish the source producer before Torch stages across devices.
126
+ torch.cuda.synchronize(tensor.device)
127
+ tensor = tensor.to(f"cuda:{device}")
128
+ tensor = tensor.contiguous()
129
+ # The producer can be on a nondefault stream. Waiting AFTER the
130
+ # driver copy is too late, and synchronize() without a device can
131
+ # wait on a completely different GPU.
132
+ torch.cuda.synchronize(device)
133
+ copy_to_buffer(volume.registered, context, tensor.data_ptr(), nbytes)
134
+ previous_unpack = _scalar(gl.glGetIntegerv(gl.GL_PIXEL_UNPACK_BUFFER_BINDING))
135
+ previous_texture = _scalar(gl.glGetIntegerv(gl.GL_TEXTURE_BINDING_3D))
136
+ try:
137
+ gl.glBindBuffer(gl.GL_PIXEL_UNPACK_BUFFER, volume.buffer)
138
+ gl.glBindTexture(gl.GL_TEXTURE_3D, volume.texture.texture_id)
139
+ with tight_unpack():
140
+ gl.glTexSubImage3D(gl.GL_TEXTURE_3D, 0, 0, 0, 0, width, height, depth,
141
+ gl.GL_RED, gl_type, None)
142
+ finally:
143
+ gl.glBindTexture(gl.GL_TEXTURE_3D, previous_texture)
144
+ gl.glBindBuffer(gl.GL_PIXEL_UNPACK_BUFFER, previous_unpack)
145
+ volume.last_version = version
146
+ return volume.texture
147
+ except Exception as error:
148
+ log_once(f"interop upload failed ({error}); falling back to cpu path")
149
+ return None
@@ -0,0 +1,137 @@
1
+ """Dropdown model functions and supporting definitions."""
2
+
3
+
4
+
5
+ def _dd_entries(container):
6
+ """Normalized (key, value, label, is_branch) rows for one level. Dict rows
7
+ read by their key (small/medium); list/tuple rows by their value
8
+ (left/center/right) since the index isn't meaningful to the user."""
9
+ if isinstance(container, dict):
10
+ items = list(container.items())
11
+ labelled = [(k, v, str(k)) for k, v in items]
12
+ else:
13
+ labelled = [(i, v, str(v)) for i, v in enumerate(container)]
14
+ return [(k, v, lbl, isinstance(v, (dict, list))) for k, v, lbl in labelled]
15
+
16
+
17
+ def _dd_subtree_matches(value, search):
18
+ """True if `search` (already lowercased) appears anywhere in this value's
19
+ subtree, so a branch stays visible while searching when a descendant matches."""
20
+ if not search:
21
+ return True
22
+ if isinstance(value, dict):
23
+ return any(search in str(k).lower() or _dd_subtree_matches(v, search)
24
+ for k, v in value.items())
25
+ if isinstance(value, list):
26
+ return any(_dd_subtree_matches(v, search) for v in value)
27
+ return search in str(value).lower()
28
+
29
+
30
+ def _dd_visible_entries(container, search=""):
31
+ """Rows shown for a level under `search`: a leaf whose label matches, or a
32
+ branch matching by label OR holding a matching descendant. Empty `search`
33
+ keeps everything."""
34
+ if not isinstance(container, (dict, list)):
35
+ return []
36
+ rows = _dd_entries(container)
37
+ if not search:
38
+ return rows
39
+ return [(k, v, lbl, br) for (k, v, lbl, br) in rows
40
+ if search in lbl.lower() or (br and _dd_subtree_matches(v, search))]
41
+
42
+
43
+ def _dd_walk(collection, path):
44
+ """Descend `collection` along a key/index `path`, returning the node there or
45
+ None if the path no longer resolves (e.g. after a search prunes it)."""
46
+ node = collection
47
+ for k in path:
48
+ try:
49
+ node = node[k]
50
+ except (KeyError, IndexError, TypeError):
51
+ return None
52
+ return node
53
+
54
+
55
+ def _dd_rows_at(collection, path, search):
56
+ """Visible rows at `path`, applying the once-a-branch-matches-by-label rule:
57
+ if any ancestor key on `path` matched the search by its own label, that whole
58
+ subtree counts as a match, so deeper levels are shown unfiltered."""
59
+ container = _dd_walk(collection, path)
60
+ ancestor_matched = bool(search) and any(search in str(k).lower() for k in path)
61
+ return _dd_visible_entries(container, "" if ancestor_matched else search)
62
+
63
+
64
+ def _dd_first_match_leaf(container, search, prefix=()):
65
+ """DFS for the path to the first selectable leaf the search reveals, so the
66
+ cursor can jump straight to it (auto-expanding the branches above). A branch
67
+ that matches by its own label contributes its first leaf unfiltered."""
68
+ for key, value, label, is_branch in _dd_visible_entries(container, search):
69
+ path = tuple(prefix) + (key,)
70
+ if not is_branch:
71
+ return path
72
+ sub_search = "" if (search and search in label.lower()) else search
73
+ sub = _dd_first_match_leaf(value, sub_search, path)
74
+ if sub is not None:
75
+ return sub
76
+ return None
77
+
78
+
79
+ def _dd_as_tuple(x):
80
+ """Coerce a stored path-state to a tuple. The states are meant to be key
81
+ tuples, but DropDownState is a DictConversion and its machinery can alias a
82
+ complex stored value (e.g. a Lora) across fields; this keeps the dropdown
83
+ robust to any input type by never iterating a non-sequence."""
84
+ if isinstance(x, tuple):
85
+ return x
86
+ if isinstance(x, list):
87
+ return tuple(x)
88
+ return ()
89
+
90
+
91
+ def _dd_path_for_value(collection, value, _depth=0):
92
+ """The key/index path of the first LEAF in `collection` equal to
93
+ `value` (depth-first through nested dicts / lists), or None when no
94
+ leaf holds it — the inverse of _dd_walk for the trigger label sync."""
95
+ if _depth > 8 or value is None:
96
+ return None
97
+ items = (collection.items() if isinstance(collection, dict)
98
+ else enumerate(collection) if isinstance(collection, (list, tuple))
99
+ else ())
100
+ for key, node in items:
101
+ if isinstance(node, (dict, list, tuple)):
102
+ sub = _dd_path_for_value(node, value, _depth + 1)
103
+ if sub is not None:
104
+ return (key,) + sub
105
+ continue
106
+ try:
107
+ same = node == value
108
+ except Exception:
109
+ same = False
110
+ if same is True:
111
+ return (key,)
112
+ return None
113
+
114
+
115
+ def _dd_label_for_path(collection, path):
116
+ """Display label for a selected leaf path: the KEY for a dict entry (e.g.
117
+ "red"), the VALUE for a list entry (e.g. "left"). Used for the trigger title
118
+ so it reads as a name, not a raw value (which may be a tuple/number)."""
119
+ if not path:
120
+ return ""
121
+ parent = _dd_walk(collection, tuple(path[:-1]))
122
+ if isinstance(parent, dict):
123
+ return str(path[-1])
124
+ return str(_dd_walk(collection, tuple(path)))
125
+
126
+
127
+ def _dd_row_lookup(mapping, value):
128
+
129
+ """mapping.get(value), tolerant of UNHASHABLE row values — a BRANCH row's
130
+ value is the nested collection dict itself, which raised TypeError from
131
+ every value-keyed style lookup (row_tags/row_tints/...)."""
132
+ if not mapping:
133
+ return None
134
+ try:
135
+ return mapping.get(value)
136
+ except TypeError:
137
+ return None
@@ -0,0 +1,73 @@
1
+ """Operations over supplied per-file dictionaries; no shared-store discovery."""
2
+ from meltygui.models.file_meta import FileMeta
3
+
4
+
5
+ def set_row_tint(meta, path, value):
6
+ """Write `value` (an rgb(a) tuple) as the tint of `path` in the shared
7
+ file-meta store, creating the entry only now — the listing never
8
+ setdefault()s entries for the files it merely shows. None (the picker's
9
+ clear) removes the tint, and the entry too when it holds nothing else,
10
+ so an unpainted file leaves no trace in file_meta.pkl."""
11
+ key = str(path)
12
+ entry = meta.get(key)
13
+ if value is None:
14
+ if isinstance(entry, dict) and dict.__contains__(entry, "tint"):
15
+ del entry["tint"]
16
+ if dict.__len__(entry) == 0:
17
+ del meta[key]
18
+ return
19
+ if not isinstance(entry, dict):
20
+ entry = meta[key] = FileMeta()
21
+ entry["tint"] = tuple(value)
22
+
23
+
24
+ def ordered_rows(rows, meta):
25
+ """`rows` ([(Path, is_dir)], natural order) sorted by the `order` stamps
26
+ in the meta store, the studio's rule (folder_files._apply_meta):
27
+ stamped rows first by their number, unstamped ones after in natural
28
+ order. No stamps at all: `rows` itself."""
29
+ if meta is None:
30
+ return rows
31
+ orders = {}
32
+ for i, (path, _is_dir) in enumerate(rows):
33
+ entry = meta.get(str(path))
34
+ if isinstance(entry, dict):
35
+ order = entry.get("order")
36
+ if isinstance(order, (int, float)):
37
+ orders[i] = order
38
+ if not orders:
39
+ return rows
40
+ indexed = sorted(range(len(rows)), key=lambda i: (orders.get(i, float("inf")), i))
41
+ return [rows[i] for i in indexed]
42
+
43
+
44
+ def set_row_order(meta, paths):
45
+ """Stamp `order` = position into the meta entry of every path of a
46
+ directory (created for the rows that have none — a reorder is the
47
+ user's explicit edit of the folder, like painting it)."""
48
+ for i, path in enumerate(paths):
49
+ key = str(path)
50
+ entry = meta.get(key)
51
+ if not isinstance(entry, dict):
52
+ entry = meta[key] = FileMeta()
53
+ if entry.get("order") != i:
54
+ entry["order"] = i
55
+
56
+
57
+ def apply_row_drop(rows, drag_keys, first_visible, drop):
58
+ """The directory's new order after `drop` (a DropEvent from on_drop, or
59
+ None): `drag_keys` are the rows that registered a drag handle this run
60
+ — the visible ones, a contiguous slice of `rows` starting at
61
+ `first_visible` — and the event's indices count in that slice. Returns
62
+ the complete [Path] order to stamp, or None when nothing moved (no
63
+ drop, a drop back in place, a cross-collection kind: rows only reorder
64
+ here)."""
65
+ if drop is None or drop.kind != "reorder" or not drag_keys:
66
+ return None
67
+ keys = list(drag_keys)
68
+ if not drop.apply(keys):
69
+ return None
70
+ paths = [path for path, _is_dir in rows]
71
+ return paths[:first_visible] + keys + paths[first_visible + len(drag_keys):]
72
+
73
+