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,1185 @@
1
+ """Collection view functions and supporting definitions."""
2
+ from collections import defaultdict
3
+ from collections import deque
4
+ from collections.abc import MutableMapping
5
+ from enum import Enum
6
+ from meltygui.core.conversion.bubbling import _BubblingDict
7
+ from meltygui.core.conversion.bubbling import _DeepPath
8
+ from meltygui.code.libcst_conversion import CallParse
9
+ from meltygui.code.libcst_conversion import ClassParse
10
+ from meltygui.code.libcst_conversion import EnumParse
11
+ from meltygui.code.libcst_conversion import FunctionParse
12
+ from meltygui.code.libcst_conversion import GeneralParse
13
+ from meltygui.hdr_color import pack_color
14
+ from meltygui.core.melty import CollectionAction
15
+ from meltygui.core.melty import Melty
16
+ from meltygui.core.melty import SearchTerm
17
+ from meltygui.core.rendering.modes import Modes
18
+ from meltygui.core.core_render import render_func
19
+ from meltygui.core.rendering.core_decoration import Core
20
+ from meltygui.core.rendering.shaped import Shaped
21
+ from meltygui.state.new_core_model import ColorPickerState
22
+ from meltygui.state.new_core_model import TabState
23
+ from meltygui.core.runtime.toggles import Toggles
24
+ from meltygui.view.header_view import draw_header
25
+ from types import NoneType
26
+ import meltygui_imgui as imgui
27
+ import types
28
+
29
+
30
+ @render_func(use_cache=False, show_bg=False, indent_size=2, disable_scroll=True,
31
+ shadow=False, selectable=False, bg_offset=1)
32
+ def draw_collection_as_tabs(input_value, tab_state: TabState = None, draw_state=None, unique=0,
33
+ excluded=None, included=None, show_excluded=False, show_system=False,
34
+ folder_type=None):
35
+ """Draws a dict as a tab bar: each inner collection gets its own tab (key = tab
36
+ name, contents via draw_any); all non-collection items are grouped into one
37
+ final "General" tab.
38
+
39
+ folder_type: a type or tuple of types that get their own tab, overriding
40
+ the default "any collection" rule — e.g. folder_type=(dict, GeneralParse)
41
+ puts dicts and GeneralParses in tabs while tuples/lists land in General.
42
+
43
+ Item filtering matches draw_collection: `excluded` names are hidden,
44
+ `included` names always show (overriding every hide rule), the type's
45
+ __excluded_attrs__ hide unless Toggles.show_excluded, and _underscored_
46
+ keys hide unless show_system. show_excluded=True disables all hiding."""
47
+ from meltygui.core.windowing.glfw_utils import request_render
48
+ from meltygui.view.decoration_view import draw_bg
49
+ from meltygui.view.tab_view import draw_tab_bar
50
+ from meltygui.core.rendering.render_dispatch import draw_any
51
+ import meltygui.core.input.drag_drop_core as _drag_drop
52
+
53
+ if excluded is None:
54
+ excluded = set()
55
+ if included is None:
56
+ included = set()
57
+ excl_attrs = getattr(type(input_value), "__excluded_attrs__", None)
58
+
59
+ def _key_visible(key):
60
+ key_str = str(key).split("##")[0]
61
+ if key_str in included:
62
+ return True
63
+ if show_excluded:
64
+ return True
65
+ if (excl_attrs is not None and not Toggles.show_excluded
66
+ and key_str in excl_attrs):
67
+ return False
68
+ if key_str in excluded:
69
+ return False
70
+ if not show_system and (key_str.startswith("_") or key_str.endswith("_")):
71
+ return False
72
+ return True
73
+
74
+ if folder_type is not None:
75
+ tab_types = folder_type
76
+ else:
77
+ tab_types = (dict, defaultdict, MutableMapping, types.MappingProxyType, list, tuple, set, deque)
78
+ visible = [(k, v) for k, v in input_value.items() if _key_visible(k)]
79
+ tab_keys = [k for k, v in visible if isinstance(v, tab_types)]
80
+ general = {k: v for k, v in visible if not isinstance(v, tab_types)}
81
+
82
+ tabs = [str(k) for k in tab_keys]
83
+ key_by_name = {str(k): k for k in tab_keys}
84
+ if general:
85
+ tabs.append("General")
86
+ if not tabs:
87
+ return False, input_value
88
+
89
+ tab_state.selected_tabs = [t for t in tab_state.selected_tabs if t in tabs]
90
+ if not tab_state.selected_tabs:
91
+ tab_state.selected_tabs = [tabs[0]]
92
+
93
+ # Tab tints come from each child's render kwargs (via return_extras below).
94
+ # The bar draws before the children, so tints lag by one frame; they're held
95
+ # on tab_state (serialized) so tabs retain their color across sessions.
96
+ if getattr(tab_state, "tab_tints", None) is None:
97
+ tab_state.tab_tints = {}
98
+ tints = [tab_state.tab_tints.get(t) for t in tabs]
99
+ # View icons ride the same one-frame-lag path as tints: each child's
100
+ # resolved `icon` kwarg (the rendered icon, e.g. from # [icon=...] comments)
101
+ # is held on tab_state and prefixed onto its tab label.
102
+ if getattr(tab_state, "tab_icons", None) is None:
103
+ tab_state.tab_icons = {}
104
+ icons = [tab_state.tab_icons.get(t) for t in tabs]
105
+
106
+ indent_size = 6
107
+ imgui.dummy(0, 5)
108
+ spacing = 8
109
+
110
+ # Framework DragDrop: this draw_state IS the drop collection (its
111
+ # input_value is the dict a Reorder/Insert applies to via
112
+ # Melty.dnd_requests + the undo tail - undo included). The tab buttons
113
+ # register as its whole-rect drag items; _dnd_horizontal makes the slot
114
+ # lines vertical gaps between tabs. dnd_keys maps each tab to its
115
+ # (dict key, dict index) - insert indices in the slots are then already
116
+ # dict-space, with non-collection keys being skipped. The synthetic
117
+ # General tab gets None: not draggable and contributes no slot.
118
+ draw_state._dnd_drop_target = True
119
+ draw_state._dnd_horizontal = True
120
+ dict_keys = list(input_value.keys())
121
+ dnd_keys = []
122
+ for t in tabs:
123
+ k = key_by_name.get(t) if not (t == "General" and t not in key_by_name) else None
124
+ dnd_keys.append((k, dict_keys.index(k)) if k is not None else None)
125
+
126
+ tab_changed, new_tabs, bar_ds = draw_tab_bar(input_value=tab_state.selected_tabs,
127
+ tab_height=30, show_bg=False, bg_offset=1,
128
+ name=f"tab_bar{unique}", wrap=True,
129
+ collection=tabs, tints=tints, icons=icons, as_toggles=False,
130
+ dnd_collection_ds=draw_state, dnd_keys=dnd_keys,
131
+ return_extras=True)
132
+ if tab_changed:
133
+ tab_state.selected_tabs = new_tabs
134
+
135
+ # The bar's view is cached, but the dragged button must be baked through
136
+ # the bar's body at the gesture edges (pickup: pick up the floating window
137
+ # kwargs + bake the placeholder; drop: restore the inline button). Between
138
+ # the edges DragDrop._keep_alive re-registers the floating window on its
139
+ # layer, so no per-frame invalidation is needed.
140
+ dnd_active_here = _drag_drop.DragDrop.active and _drag_drop.DragDrop.source_ds is draw_state
141
+ if dnd_active_here != getattr(draw_state, "_tab_dnd_was_active", False):
142
+ draw_state._tab_dnd_was_active = dnd_active_here
143
+ if bar_ds is not None:
144
+ request_render()
145
+
146
+ imgui.dummy(0, 2)
147
+ changed = False
148
+ # Rendered content views in visual order, as (dict_index, draw_state) -
149
+ # for the between-content drop slots published below.
150
+ content_stack = []
151
+ # Selected tabs stack vertically in tab-bar order (no columns).
152
+ for tab in tabs:
153
+ if tab not in tab_state.selected_tabs:
154
+ continue
155
+ content_dragged = False
156
+ if tab == "General" and tab not in key_by_name:
157
+ general_changed, new_general, child_ds = draw_any(general, name=f"Tab: General {unique}",
158
+ disable_scroll=False, indent_size=indent_size,
159
+ use_cache=True, return_extras=True)
160
+ if general_changed:
161
+ for k, v in new_general.items():
162
+ input_value[k] = v
163
+ changed |= general_changed
164
+ else:
165
+ key = key_by_name[tab]
166
+ # With several tabs open, each content view is ALSO a drag item of
167
+ # this dict (key= and _collection_draw_state to make its header a
168
+ # pickup handle from DragDrop.register_item) - dragging a stacked
169
+ # collection reorders the tabs just like dragging a tab button.
170
+ # The item_ds identity check disambiguates the two views sharing
171
+ # (collection, key): only the one actually picked up floats.
172
+ multi = len(tab_state.selected_tabs) > 1
173
+ content_extra = {}
174
+ content_dragged = False
175
+ if multi:
176
+ content_extra["key"] = key
177
+ prev_ds = (getattr(draw_state, "_tab_child_ds", None) or {}).get(tab)
178
+ if (prev_ds is not None and _drag_drop.DragDrop.item_ds is prev_ds
179
+ and _drag_drop.DragDrop.is_dragged_child(draw_state, key)):
180
+ content_dragged = True
181
+ content_extra.update(_drag_drop.DragDrop.dragged_item_kwargs())
182
+
183
+ content_extra["use_cache"] = True
184
+ tab_content_changed, value, child_ds = draw_any(input_value[key], name=f"{tab}",
185
+ disable_scroll=False, indent_size=indent_size,
186
+ return_extras=True,
187
+ **content_extra)
188
+ if child_ds is not None:
189
+ # Membership follows multi-select: cleared when only one tab
190
+ # is open so the lone content view stops being a pickup handle.
191
+ child_ds._collection_draw_state = draw_state if multi else None
192
+ if not content_dragged:
193
+ content_stack.append((dict_keys.index(key), child_ds))
194
+ if content_dragged:
195
+ # The dragged content deferred to a floating window - hold its
196
+ # vertical slot open at its pickup rect (placeholder draws its
197
+ # own item spacing).
198
+ _drag_drop.DragDrop.draw_placeholder(False, spacing,
199
+ style_manager=Core.melty.style_manager,
200
+ draw_bg=draw_bg)
201
+ if tab_content_changed:
202
+ input_value[key] = value
203
+ changed |= tab_content_changed
204
+
205
+ if not content_dragged:
206
+ # (the placeholder path already added its own item spacing)
207
+ imgui.dummy(0, spacing)
208
+
209
+ if child_ds is not None:
210
+ # Track each tab's content draw_state so deselected tabs can be
211
+ # marked hidden below (they stop rendering but keep stale
212
+ # geometry, which leaked DragDrop drop lines).
213
+ if getattr(draw_state, "_tab_child_ds", None) is None:
214
+ draw_state._tab_child_ds = {}
215
+ draw_state._tab_child_ds[tab] = child_ds
216
+ child_ds._hidden_offscreen = False
217
+
218
+ child_tint = child_ds._kwargs.get("tint", None) if child_ds is not None else None
219
+ if child_tint is not None and tab_state.tab_tints.get(tab) != child_tint:
220
+ tab_state.tab_tints[tab] = child_tint
221
+ draw_state.invalidate()
222
+
223
+ child_icon = child_ds._kwargs.get("icon", None) if child_ds is not None else None
224
+ if child_icon is not None and tab_state.tab_icons.get(tab) != child_icon:
225
+ tab_state.tab_icons[tab] = child_icon
226
+ draw_state.invalidate()
227
+
228
+ # Deselected tabs' content: not collapsed, not closed, but no longer
229
+ # rendered - so nothing downstream knows it's invisible. Stamp
230
+ # _hidden_offscreen (the same flag end_frame uses for spawner-scrolled
231
+ # nested tiles) so DragDrop's slot sweep skips their whole subtrees.
232
+ hidden_reg = getattr(draw_state, "_tab_child_ds", None)
233
+ if hidden_reg:
234
+ for t, t_ds in hidden_reg.items():
235
+ if t not in tab_state.selected_tabs or t not in tabs:
236
+ t_ds._hidden_offscreen = True
237
+
238
+ # Publish drop slots BETWEEN the stacked content views (the framework only
239
+ # derives slots from _children, which here are the tab buttons): one
240
+ # horizontal line above each content (insert before its dict key) and one
241
+ # below the last (append after it). Rebuilt from live geometry every
242
+ # render, so they track scroll/reflow; a dragged content is excluded (its
243
+ # home slot is the cancel target). Insert indices are dict-space,
244
+ # same as the bar slots.
245
+ extra_slots = []
246
+ for idx, c_ds in content_stack:
247
+ top, left = c_ds.abs_top, c_ds.abs_left
248
+ if top is None or left is None or not c_ds.width:
249
+ continue
250
+ extra_slots.append((idx, left, left + c_ds.width, top - 2, False))
251
+ if extra_slots:
252
+ last_idx, last_ds = content_stack[-1]
253
+ if last_ds.abs_top is not None and last_ds.abs_left is not None and last_ds.width:
254
+ extra_slots.append((last_idx + 1, last_ds.abs_left,
255
+ last_ds.abs_left + last_ds.width,
256
+ last_ds.abs_top + (last_ds.height or 0) + 3, False))
257
+ draw_state._dnd_extra_slots = extra_slots
258
+
259
+ return changed, input_value
260
+
261
+
262
+ @render_func(is_default_for=(dict, MutableMapping, defaultdict, tuple, list, GeneralParse,
263
+ CallParse, ClassParse, EnumParse, FunctionParse, _BubblingDict, _DeepPath),
264
+ use_cache=True, header_same_line=False, show_bg=True, show_instance_vars=False, align_header=False,
265
+ manual_content_height=True, shadow=True, selectable=False, bg_offset=-0.8,
266
+ wrap=False, with_header=draw_header, indent_size=3, searchable=True, child_kwargs=None)
267
+ def draw_collection(input_value, draw_state, depth, style_manager, meta, icon=None,
268
+ mode=None, keys=None, get_attr=None, set_attr=None, show_excluded=False,
269
+ child_kwargs=None, show_bg=False, show_search=False, align_header=False, wrap=False,
270
+ on_collapse=False, search_text="", return_item=False, close_triggers_delete=False,
271
+ on_expand=False, show_add_delete=False, show_add_types=None, item_spacing_y=3, show_system=False,
272
+ included=None, horizontal=False, show_indices=False, excluded=None, annotation=None,
273
+ drop_tail_height=None, **kwargs):
274
+ """
275
+ Universal collection renderer
276
+ show_add_types={"Display Name": TypeA, ...} draws a second + button in the
277
+ header that instantiates the chosen type (rendered by draw_header; the
278
+ value just rides the kwargs through). Several entries get a chevron
279
+ dropdown to pick from; a single entry binds the + directly with no
280
+ chevron. A bare list of types is accepted and keyed by __name__.
281
+ """
282
+ from meltygui.core.conversion.render_host import RenderHost
283
+ from meltygui.editor.text_editor import _scroll_into_view
284
+ from meltygui.core.windowing.glfw_utils import print_stack_trace
285
+ from meltygui.view.decoration_view import draw_bg
286
+ from meltygui.view.header_view import draw_header_end
287
+ from meltygui.core.cache.tile_cache import snap_int
288
+ from meltygui.model.collection_model import annotation_item_type
289
+ from meltygui.model.collection_model import _collection_match_keys
290
+ from meltygui.model.search_model import _fuzzy_key_match
291
+ from meltygui.core.rendering.render_dispatch import draw_any
292
+ from meltygui.core.rendering.render_dispatch import seperator
293
+ import meltygui.core.input.drag_drop_core as _drag_drop
294
+
295
+
296
+
297
+
298
+ if excluded is None:
299
+ excluded = set()
300
+
301
+ if included is None:
302
+ included = set()
303
+
304
+ if child_kwargs is None:
305
+ child_kwargs = {}
306
+
307
+ # A RenderHost rendered DIRECTLY (draw_collection(host) - the settings
308
+ # column of draw_space_mouse, the modifies playground) is this view's
309
+ # delegate, so pulse it: notify_on_change is the per-frame liveness
310
+ # stamp that idle sweep (RenderHost.sweep) reads, so it re-registers a
311
+ # swept host. Without the pulse an evictable code host was deregistered
312
+ # after idle_frames, the wrapper never drew again, and an edit eve
313
+ # dirtied the dict but the outbound chain (dict -> cst -> source -> save)
314
+ # never ran - the code never updated (Lukas 09-04). Idle frames skip
315
+ # the body, so a swept host revives on the next frame an edit re-runs it.
316
+ if isinstance(input_value, RenderHost):
317
+ input_value.notify_on_change(draw_state)
318
+
319
+ changed = False
320
+
321
+ if hasattr(input_value, 'children') and isinstance(input_value.children, (list, dict, defaultdict,
322
+ types.MappingProxyType, deque)):
323
+ input_value = input_value.children
324
+
325
+ if show_bg and draw_state.total_z_offset < 0:
326
+ imgui.dummy(1, 3)
327
+ else:
328
+ imgui.dummy(1, 1)
329
+
330
+ # --- configure per collection type ---
331
+ collection = input_value
332
+
333
+ # When the value *is* a class (e.g. an @window-registered class drawn
334
+ # directly), its per-attribute `{field}_meta` overrides live on the class
335
+ # itself, not on its metaclass. Use the class as parent_type so get_child_meta
336
+ # can find them; otherwise fall back to the instance's class.
337
+ parent_type = input_value if isinstance(input_value, type) else input_value.__class__
338
+ if isinstance(input_value, (str, int, float, bool, Enum, NoneType)):
339
+ imgui.text("No view for type: " + str(type(input_value)))
340
+ return False, input_value
341
+ if keys is None:
342
+ if isinstance(input_value,
343
+ (dict, list, tuple, set, defaultdict, MutableMapping, types.MappingProxyType, _DeepPath, deque)):
344
+ apply_change = True
345
+ parent_type = input_value.__class__
346
+ if isinstance(input_value, types.MappingProxyType):
347
+ keys = input_value.keys()
348
+ elif isinstance(input_value, (dict, defaultdict, MutableMapping, types.MappingProxyType)):
349
+ keys = input_value.keys()
350
+ collection = input_value
351
+ else:
352
+ keys = range(len(input_value))
353
+ collection = list(input_value)
354
+
355
+ elif hasattr(input_value, "__dict__") and depth < Core.melty.max_depth:
356
+ if hasattr(type(input_value), "__field_defaults__") and hasattr(input_value, 'to_dict'):
357
+ type(input_value).__field_defaults__.update(input_value.__dict__)
358
+ # __field_defaults__ accumulates keys from every instance, so a
359
+ # field deleted from THIS instance lingers there. Skip keys the
360
+ # instance no longer resolves (neither set nor the class default)
361
+ # so deleting a field removes its row instead of leaving a ghost.
362
+ keys = [k for k in type(input_value).__field_defaults__
363
+ if k in input_value.__dict__ or hasattr(input_value, k)]
364
+ else:
365
+ if input_value is None or input_value.__dict__ is None:
366
+ return False, input_value
367
+ keys = input_value.__dict__.keys()
368
+ collection = input_value.__dict__
369
+ use_tint = False
370
+ apply_change = True
371
+ else:
372
+ imgui.text("No view for type: " + str(type(input_value)))
373
+ return False, input_value
374
+
375
+ keys = list(keys)[:]
376
+
377
+ # --- per-key type annotations ---
378
+ # Class-level annotations (walking the MRO) name a type per attribute when
379
+ # rendering an object's __dict__; a typed container annotation
380
+ # (Dict[str, Lora] / List[Lora]) covers every key otherwise. Each child
381
+ # inherits its annotation so its own add button can instantiate the right
382
+ # item type (replaces the old meta.field_type path).
383
+ parent_annotations = {}
384
+ for _klass in reversed(getattr(parent_type, "__mro__", ())):
385
+ _anns = _klass.__dict__.get("__annotations__")
386
+ if _anns:
387
+ parent_annotations.update(_anns)
388
+ item_annotation = annotation_item_type(annotation)
389
+
390
+ # --- search (key matching) ---
391
+ # Same dual resolution as the text editor: a forwarded SearchTerm carries
392
+ # the shared cross-view session, or the owner's own session when this
393
+ # collection hosts the find UI. We claim one slot per matching key, in
394
+ # visual order interleaved with the children (claimed below), so the
395
+ # combined next/prev sequence reads top-to-bottom. The current key is
396
+ # latched so incidental repaints don't shift the highlight.
397
+ _search_term = search_text or (draw_state.search_text if draw_state.search_active else "")
398
+ if isinstance(_search_term, SearchTerm):
399
+ search_session = _search_term
400
+ elif draw_state.search_active and draw_state._search_session is not None:
401
+ search_session = draw_state._search_session
402
+ else:
403
+ search_session = None
404
+ search_q = str(_search_term).lower() if (search_session is not None and _search_term) else ""
405
+ search_current_y = None # screen-Y of the current key's row (for scroll)
406
+ search_current_h = None
407
+ # On a full-search frame (term change / nav) render every row — even ones
408
+ # the off-screen optimization would skip — so the row holding the current
409
+ # match is reached and can scroll into view.
410
+ _search_full_render = search_session is not None and search_session.scroll_to
411
+
412
+ # Stash a matcher so the search owner's tree walk (DrawState.descendants /
413
+ # meltygui.search_walk) can count this collection's key matches without
414
+ # rendering. It counts only this collection's own keys (the whole key list,
415
+ # not just the rows the loop below draws); child collections / text editors
416
+ # are separate tree nodes with their own matchers, so the walk sums them
417
+ # without double-counting. Keys are re-derived lazily on call (only on
418
+ # counting frames), so an unsearched render pays nothing for it.
419
+ def _search_matcher(term, session, _iv=input_value, _keys=keys,
420
+ _excl=excluded, _se=show_excluded):
421
+ q = str(term).lower()
422
+ if not q:
423
+ return
424
+ mk = _collection_match_keys(_iv, _keys, _excl, _se)
425
+ session.claim(sum(1 for _, k in mk if _fuzzy_key_match(q, k)))
426
+
427
+ draw_state._search_matcher = _search_matcher
428
+
429
+ # The owner's pre-body walk picked the global-current match and stashed its
430
+ # local index on us (_search_active_local) when one of OUR keys holds it —
431
+ # the same walk that produced the count, so selection and count agree.
432
+ # Resolve that ordinal (over our matching keys, in key order) to the key
433
+ # index the loop should highlight + scroll to. None when the current match
434
+ # lives in a child instead (that child carries its own mark).
435
+ _current_key_idx = None
436
+ if search_q and draw_state._search_active_local is not None:
437
+ _matching = [i for (i, k) in
438
+ _collection_match_keys(input_value, keys, excluded, show_excluded)
439
+ if _fuzzy_key_match(search_q, k)]
440
+ if 0 <= draw_state._search_active_local < len(_matching):
441
+ _current_key_idx = _matching[draw_state._search_active_local]
442
+
443
+ # --- unified loop ---
444
+ drew_any = False
445
+
446
+ start_cursor = imgui.get_cursor_pos()[1]
447
+ rect = Core.melty.get_clip_rect()
448
+
449
+ premature_break = False
450
+
451
+ Core.melty.collection_index_stack.append(0)
452
+ this_collection = len(Core.melty.collection_index_stack) - 1
453
+
454
+ max_items = 5000
455
+ start_index = 0
456
+ end_index = min(len(keys) - 1, max_items)
457
+
458
+ scroll_offset = draw_state.scroll_offset
459
+ true_left = draw_state.left - scroll_offset[0]
460
+ true_top = draw_state.top - scroll_offset[1]
461
+
462
+ # Remove excluded from keys
463
+ item_to_return = None
464
+ # Keys of children whose close (X) was clicked this frame — collected during the
465
+ # loop and removed from the collection AFTER it (never mutate keys mid-iteration).
466
+ to_delete = set()
467
+ # Whether any row was row-skipped this pass - a skipped pass reconstructs
468
+ # the layout from cached relative_pos/height caches, so its accuracy is only
469
+ # as good as those caches; a skip-free pass measured everything for real.
470
+ rows_skipped = False
471
+
472
+ for idx in range(start_index, end_index + 1):
473
+ key = keys[idx]
474
+ relative_pos = imgui.get_cursor_screen_pos()
475
+ relative_pos = (relative_pos[0] - true_left,
476
+ relative_pos[1] - true_top + item_spacing_y)
477
+
478
+ child_draw_state = draw_state._children.get(idx, None)
479
+
480
+ # A child currently being drag-and-dropped renders as a floating
481
+ # window (kwargs injected below) - never row-skip it, its stale
482
+ # relative_pos no longer says where it is.
483
+ is_dragged = _drag_drop.DragDrop.is_dragged_child(draw_state, key)
484
+
485
+ # ----- off-screen detection -----
486
+ # Is this row scrolled outside the viewport? When not searching, skip it
487
+ # entirely up front (the perf early-out). On a search-counting frame keep
488
+ # `clipped` to decide below: reuse a cached match count (no render) or
489
+ # render to (re)count.
490
+ clipped = False
491
+ if (not horizontal and not is_dragged and child_draw_state is not None
492
+ and child_draw_state.relative_pos is not None
493
+ and not Core.melty.frame_count <= 2
494
+ and (not draw_state.invalid_content_height or imgui.is_mouse_down(0)
495
+ or imgui.is_mouse_down(1) or imgui.is_mouse_down(2))):
496
+ _spy = true_top + child_draw_state.relative_pos[1] - child_draw_state.header_height
497
+ _bottom = _spy + child_draw_state.height + child_draw_state.header_height
498
+ clipped = (_bottom + child_draw_state.height < rect[1] or _spy > rect[3])
499
+
500
+ if clipped and not _search_full_render:
501
+ rows_skipped = True
502
+ imgui.set_cursor_screen_pos((imgui.get_cursor_screen_pos()[0],
503
+ (true_top + child_draw_state.relative_pos[1] +
504
+ child_draw_state.height)))
505
+ continue
506
+
507
+ Core.melty.collection_index_stack[this_collection] = idx
508
+ item = None
509
+ if get_attr is None:
510
+ if isinstance(collection, dict) and key not in collection:
511
+ # A declared attr deleted from the instance __dict__ still
512
+ # resolves through the class default - use that instead of
513
+ # degrading to a "Key not found" ghost row.
514
+ if hasattr(input_value, "__dict__") and hasattr(input_value, str(key)):
515
+ item = getattr(input_value, str(key), None)
516
+ else:
517
+ imgui.text("Key not found: " + str(key))
518
+ continue
519
+ elif hasattr(input_value, "__dict__") and hasattr(input_value, str(key)):
520
+ item = getattr(input_value, str(key), None)
521
+ else:
522
+ item = collection[key]
523
+ else:
524
+ try:
525
+ item = get_attr(input_value, key)
526
+ except Exception as e:
527
+ imgui.text(f"Error getting key {key}")
528
+
529
+ # visual separator (object extras)
530
+ if key is None and item is None:
531
+ seperator(Core.melty.spacing[1])
532
+ continue
533
+ # apply global skip to all types
534
+ if isinstance(key, (float, Enum, NoneType)):
535
+ key_str = f"{input_value.__class__.__name__}"
536
+ elif isinstance(key, int):
537
+ key_str = f"{key}"
538
+ else:
539
+ key_str = str(key)
540
+
541
+ if not show_excluded and hasattr(type(input_value), "__excluded_attrs__"):
542
+ if not Toggles.show_excluded:
543
+ if key_str in type(input_value).__excluded_attrs__ and key_str not in included:
544
+ continue
545
+ display_name = None
546
+
547
+ if key_str in excluded and key_str not in included:
548
+ continue
549
+
550
+ if not show_excluded and (not show_system and (key_str.startswith("_") or key_str.endswith("_"))):
551
+ if key_str not in included:
552
+ continue
553
+
554
+ # ----- SEARCH (key match) -----
555
+ # Whether this key is the global-current match was decided by the
556
+ # owner's pre-body walk (resolved to _current_key_idx above); we just
557
+ # flag it and record its row Y so the post-loop block scrolls to it.
558
+ key_is_match = bool(search_q) and _fuzzy_key_match(search_q, key_str.lower())
559
+ key_is_current = key_is_match and idx == _current_key_idx
560
+ if key_is_current:
561
+ search_current_y = imgui.get_cursor_screen_pos()[1]
562
+
563
+ prev_tint = None
564
+ try:
565
+
566
+ y_offset = Core.melty.collection_spacing
567
+ if show_indices:
568
+ display_name = f"{str(idx)}"
569
+
570
+ item_kwargs = {
571
+ 'type_collection': kwargs.get("real_type", type(input_value)),
572
+ 'real_type': type(item),
573
+ 'return_extras': True,
574
+ 'key': key,
575
+ 'on_collapse': on_collapse,
576
+ 'on_expand': on_expand,
577
+ 'collection': input_value,
578
+ 'name': key_str,
579
+ 'display_name': display_name,
580
+ 'parent_show_add_delete': show_add_delete,
581
+ 'show_add_delete': show_add_delete,
582
+ 'with_header_end': draw_header_end if show_add_delete else None,
583
+ 'annotation': parent_annotations.get(key, item_annotation),
584
+ 'y_offset': y_offset,
585
+ 'mode': mode,
586
+ 'wrap': wrap,
587
+ 'search_match': key_is_match,
588
+ 'search_current': key_is_current,
589
+ }
590
+
591
+ if "content_width" in draw_state._kwargs:
592
+ item_kwargs['content_width'] = draw_state._kwargs['content_width'] - Core.melty.spacing[0] * 2
593
+
594
+ # Folders: a dict child of a typed-add collection (show_add_types)
595
+ # inherits the same type choices, so nested folders keep the
596
+ # [+ <type> v] affordance all the way down.
597
+ if show_add_types and isinstance(item, dict):
598
+ item_kwargs['show_add_types'] = show_add_types
599
+
600
+ # Per-field `# [tint=...]` comment overrides
601
+ # (__overrides__['__<field>__'] from the parent) are fed by the
602
+ # render_func wrapper from the collection= + key= passed above -
603
+ # see the __overrides__ block in core_render.
604
+
605
+ item_kwargs = item_kwargs | child_kwargs
606
+ if is_dragged:
607
+ # Detach the dragged child to a floating closable window,
608
+ # pinned to its pre-pickup size (the wrapper glues the
609
+ # window_pos to the cursor each dispatch).
610
+ item_kwargs.update(_drag_drop.DragDrop.dragged_item_kwargs())
611
+ if isinstance(input_value, (list, tuple, set)) or horizontal:
612
+ item_kwargs['align_header'] = False
613
+
614
+ if horizontal and child_draw_state is not None:
615
+ rect = Core.melty.get_clip_rect()
616
+ right_edge = rect[2]
617
+ space_left = right_edge - (imgui.get_cursor_screen_pos()[0] + child_draw_state.width)
618
+
619
+ if len(child_draw_state._children) > 0 and child_draw_state.height > 50:
620
+ imgui.dummy(0, 0)
621
+
622
+ elif space_left < 0:
623
+ imgui.new_line()
624
+ imgui.dummy(0, item_spacing_y)
625
+
626
+ item_return = draw_any(item, **item_kwargs)
627
+
628
+ if len(item_return) == 3:
629
+ item_changed, out_val, returned_ds = item_return
630
+ else:
631
+ item_changed, out_val, returned_ds = item_return[0], item_return[1], None
632
+
633
+ if returned_ds is not None:
634
+ draw_state._children[idx] = returned_ds
635
+ returned_ds._collection_draw_state = draw_state
636
+ returned_ds.relative_pos = relative_pos
637
+ # Child window closed via its X (closable + closed) -> queue its key
638
+ # for removal from the collection (applied after the loop).
639
+ if returned_ds.closable and returned_ds.closed:
640
+ if close_triggers_delete:
641
+ to_delete.add(key)
642
+ if key_is_current:
643
+ search_current_h = returned_ds.header_height
644
+ if is_dragged:
645
+ # The child deferred to a floating window and drew nothing
646
+ # inline — hold its slot open with a placeholder so the
647
+ # collection layout doesn't shift. Neither the horizontal
648
+ # branch nor any other math off returned_ds may run here:
649
+ # returned_ds.abs_* is the floating window glued to the
650
+ # mouse, so a mid-drag re-render would measure the flow
651
+ # back of the cursor. The placeholder anchors itself to the
652
+ # pickup slot (DragDrop.home_rect).
653
+ _drag_drop.DragDrop.draw_placeholder(horizontal, item_spacing_y,
654
+ style_manager=style_manager,
655
+ draw_bg=draw_bg)
656
+ elif horizontal:
657
+ imgui.same_line(spacing=0)
658
+ imgui.set_cursor_screen_pos((returned_ds.abs_left + returned_ds.width, returned_ds.abs_top))
659
+ else:
660
+ imgui.dummy(0, item_spacing_y)
661
+
662
+ if isinstance(out_val, CollectionAction):
663
+ # perform the move; this should mutate the plain dicts you attached
664
+ result = Core.melty.to_apply(out_val)
665
+ item_changed, out_val = False, None
666
+
667
+ if set_attr is not None and item_changed:
668
+ try:
669
+ set_attr(input_value, key, out_val)
670
+ except Exception as e:
671
+ print(f"Error setting key {key} to value {out_val}: {e}")
672
+ else:
673
+ if "return_item" not in item_kwargs:
674
+ if item_changed and apply_change and key is not None:
675
+ if isinstance(input_value, (dict, defaultdict, MutableMapping, types.MappingProxyType)):
676
+ input_value[key] = out_val
677
+ elif isinstance(input_value, list):
678
+ input_value[key] = out_val
679
+ elif isinstance(input_value, deque):
680
+ input_value[key] = out_val
681
+ elif isinstance(input_value, tuple):
682
+ temp = list(input_value)
683
+ temp[key] = out_val
684
+ input_value = parent_type(temp)
685
+ else:
686
+ setattr(input_value, key_str, out_val)
687
+
688
+ changed |= item_changed
689
+ if item_changed and return_item:
690
+ item_to_return = out_val
691
+
692
+ except Exception as e:
693
+ print(f"Error rendering field '{key_str}' of {type(input_value).__name__}: {e}")
694
+ print_stack_trace(exception=e)
695
+
696
+ finally:
697
+ if prev_tint is not None:
698
+ style_manager.set_imgui_tint(*prev_tint)
699
+
700
+ Core.melty.collection_index_stack.pop()
701
+
702
+ # Apply removals for children closed via their X this frame (collected above, so the
703
+ # collection is never mutated mid-iteration). Only dict-like / list collections are
704
+ # safely key-deletable here; tuples/sets/object-__dict__ are left untouched. Sets
705
+ # `changed` so the edit propagates to the owner (e.g. a RenderHost io_function).
706
+ if to_delete:
707
+ print(f"Deleting keys {to_delete} {input_value.__class__.__name__}")
708
+ if isinstance(input_value, (dict, defaultdict, MutableMapping, _BubblingDict)):
709
+ for _k in to_delete:
710
+ print(f"_k in to_delete Deleting key {_k}")
711
+ if _k in input_value:
712
+ print(f"del input_value[_k found in, deleting")
713
+ del input_value[_k]
714
+ changed = True
715
+
716
+
717
+ elif isinstance(input_value, list):
718
+ for _i in sorted((k for k in to_delete if isinstance(k, int)), reverse=True):
719
+ if 0 <= _i < len(input_value):
720
+ del input_value[_i]
721
+ changed = True
722
+
723
+ # When navigation just happened, scroll the current key into view.
724
+ # draw_collection disables its own scroll, so _scroll_into_view walks up to
725
+ # the real scroll container. (A current match inside a child is scrolled by
726
+ # that child itself.) _current_key_idx came from the owner's walk.
727
+ if search_session is not None and search_session.scroll_to:
728
+ draw_state._search_current_key = _current_key_idx
729
+ if search_current_y is not None:
730
+ h = search_current_h or imgui.get_text_line_height()
731
+ _scroll_into_view(draw_state, search_current_y, search_current_y + h)
732
+
733
+ # Prune stale child slots - indices outside the current key range, left
734
+ # over from deletions/cross-collection edits. Their draw_states keep old
735
+ # geometry that ghost-walks (skip-advance, drop slots) would trip over.
736
+ if draw_state._children:
737
+ for _stale_idx in [i for i in draw_state._children
738
+ if isinstance(i, int) and i > end_index]:
739
+ del draw_state._children[_stale_idx]
740
+
741
+ # Static drop tail for drag-and-drop collections: a fixed strip of empty
742
+ # space below the last row. Its height counts towards the collection's
743
+ # measured height, which pushes the PARENT's "append to folder" slot down
744
+ # - without it a folder ends right at its last child, so that slot and the
745
+ # nested collection's own "append at end" slot (last child bottom + 3) land
746
+ # nearly on top of each other. Also gives an empty collection a droppable
747
+ # body. Always present (NOT drag-conditional, per Lukas) so layout never
748
+ # shifts when a drag begins. Gated to dict/list here - exactly what
749
+ # the DnD system treats as a drop target (see drag_drop._is_target_collection).
750
+ # Height comes live from Toggles.Collection.drop_tail_height (so the Toggles
751
+ # UI edits it globally); a per-call drop_tail_height= kwarg overrides it.
752
+ _drop_tail = (drop_tail_height if drop_tail_height is not None
753
+ else Toggles.Collection.drop_tail_height)
754
+ if (_drop_tail and not horizontal
755
+ and isinstance(draw_state._raw_input_value, (dict, list))):
756
+ tail_h = int(_drop_tail)
757
+ # Single-line collections (the wrapper same-line's the body BESIDE the
758
+ # header when not multi_line - e.g. a small dict) need the tail to
759
+ # also span the header height, or a bare vertical dummy sits to the
760
+ # right of the header and never grows the box past header height. Add
761
+ # the header height so the tail reaches BELOW the header, giving an
762
+ # empty collection a real droppable body. Multi-line collections
763
+ # already stack the body under the header, so a bare tail is fine.
764
+ if not draw_state.multi_line:
765
+ tail_h += int(draw_state.header_height or 0)
766
+ imgui.dummy(1, tail_h)
767
+
768
+ end_pos = imgui.get_cursor_pos()[1]
769
+ content_height = (end_pos - start_cursor)
770
+ imgui.dummy(1, 0)
771
+
772
+ # Commit the measurement when it's trustworthy: a skip-free pass measured
773
+ # every row for real (commit even mid-drag - that's what lets a collection
774
+ # update DURING a drag instead of storming after it); a pass with
775
+ # skips reconstructed the layout from caches, so only commit it in the
776
+ # old steady-state conditions (no buttons held).
777
+ measured_fully = not rows_skipped and not premature_break
778
+ if measured_fully or (not imgui.is_mouse_down(0) and not imgui.is_mouse_down(1)
779
+ and not imgui.is_mouse_down(2) and not Melty.space_mouse_drag
780
+ and not premature_break):
781
+ draw_state.content_height = snap_int(content_height)
782
+ draw_state.invalid_content_height = False
783
+
784
+ draw_state.premature_break = premature_break
785
+ if return_item:
786
+ if changed:
787
+ return changed, item_to_return
788
+ else:
789
+ return False, input_value
790
+
791
+ return changed, input_value
792
+
793
+
794
+ @render_func(is_default_for=(
795
+ 'tint', 'help_yellow_tint', 'color', 'context_select_tint', "text_color", "gradient_color", "outline_color",
796
+ # Any 3- or 4-tuple of numbers with a float in it is a colour (promoted
797
+ # dtype: `(0, 0, 0, 0.1)` works, an all-int `(1, 2, 3)` doesn't). The
798
+ # name entries above also catch plain tints like `tint=(0, 0, 0)`.
799
+ Shaped(tuple, (3,), float), Shaped(tuple, (4,), float)),
800
+ has_popup=True,
801
+ indent_size=2, is_tree=False, align_header=True, header_same_line=True, wrap=True,
802
+ show_name=True, selectable=False, max_width=100, min_width=33, use_cache=False, with_header=draw_header)
803
+ def draw_tuple(input_value: tuple | types.NoneType, name, unique, draw_state, outline=False,
804
+ info=None):
805
+ from meltygui.core.windowing.glfw_utils import request_render
806
+ from meltygui.view.color_view import draw_color_picker
807
+ from meltygui.view.control_view import button
808
+ from meltygui.view.color_view import color_picker_height
809
+ from meltygui.view.color_view import color_picker_top_offset
810
+ from meltygui.view.color_view import color_picker_width
811
+ import meltygui.core.windowing.window_api as glfw
812
+
813
+ is_open = False
814
+ changed = False
815
+
816
+ if input_value is None:
817
+ # Position BEFORE drawing, exactly like the color branch below - a
818
+ # first frame that skips the same_line breaks the header row (every
819
+ # remaining item wraps to a new line) and the early return under the
820
+ # button never restores it. Single-line rows only: line placement
821
+ # belongs in the wrapper (it same_lines after the header unless it
822
+ # chose multi_line), and forcing same_line on a multi_line frame drags
823
+ # the widget back onto the header's line.
824
+ if not draw_state.multi_line:
825
+ imgui.same_line(spacing=4)
826
+ if button("", height=21, shadow=False, z_offset=0, corner_radius=4, tint=(0, 0, 0, 0.1
827
+ ), tint_value=0.14, use_cache=True,
828
+ show_bg=True, text_pad=7, name=f"add_tuple##{unique}",
829
+ show_button_bg=True)[0]:
830
+ input_value = (0.0, 0.0, 0.0, 1.0)
831
+ request_render()
832
+ return True, input_value
833
+
834
+ else:
835
+ # if not draw_state.multi_line:
836
+ # imgui.same_line(spacing=4)
837
+
838
+ is_color = input_value is not None and isinstance(input_value, tuple) and len(input_value) in (3, 4) and all(
839
+ isinstance(c, (float, int)) for c in input_value)
840
+ if is_color:
841
+ # A swatch trigger that opens our own colour-picker popover (replacing
842
+ # imgui's built-in popup). Same popover pattern as the dropdown: identity
843
+ # in Melty.popover_focused_ds is the open state; click toggles it; the
844
+ # picker window is anchored under the swatch and dismissed on outside
845
+ # click / Esc. The picker itself is stateless and returns the new colour.
846
+ from meltygui.core.rendering.mode import Mode
847
+ is_open = Melty.popover_focused_ds is draw_state
848
+ col = list(input_value)
849
+ alpha = col[3] if len(col) == 4 else 1.0
850
+ # ALPHA_PREVIEW_HALF makes the swatch split: one half shows the colour
851
+ # composited over a checkerboard at its real alpha, the other fully
852
+ # opaque - so a length-4 tuple's transparency is visible in the chip itself
853
+ # (plain color_button forces opaque regardless of the alpha we pass).
854
+ flags = imgui.COLOR_EDIT_NO_TOOLTIP | imgui.COLOR_EDIT_ALPHA_PREVIEW_HALF
855
+ if imgui.color_button(f"##swatch{unique}{name}", col[0], col[1], col[2], alpha,
856
+ flags=flags, width=17, height=17):
857
+ Melty.popover_focused_ds = None if is_open else draw_state
858
+ if not is_open:
859
+ Melty._popover_open_frame = Melty.frame_count # grace the opening click
860
+ request_render()
861
+ if outline:
862
+ # Tight ring around the CHIP (the item just drawn) - the
863
+ # widget's draw_state box is the measured min/max_width
864
+ # envelope, far wider than the swatch.
865
+ _omn, _omx = imgui.get_item_rect_min(), imgui.get_item_rect_max()
866
+ imgui.get_window_draw_list().add_rect(
867
+ _omn.x - 1.5, _omn.y - 1.5, _omx.x + 1.5, _omx.y + 1.5,
868
+ pack_color(1.0, 1.0, 1.0, 0.55), rounding=4.0)
869
+ is_open = Melty.popover_focused_ds is draw_state # reflect the toggle this frame
870
+
871
+ # The picker window is closable -> fixed size (auto-resize is off for
872
+ # closable windows), and its content is raw imgui (not child render_funcs)
873
+ # so the framework can't measure it. Size the window to fit the SV square
874
+ # (180) + the N channel drag-floats and the hex line, so nothing clips.
875
+ # info: a general-purpose caption for the popover (e.g. the anywhere
876
+ # swatch's "last known source"). A CALLABLE resolves only while the
877
+ # popover is open for a lazy path, so closed swatches never pay for it.
878
+ _info = None
879
+ if is_open and info is not None:
880
+ _info = info() if callable(info) else info
881
+ picker_h = color_picker_height(4, bool(_info))
882
+ # parent_window=draw_state anchors the popover under the swatch and makes
883
+ # the tuple the picker's ancestor, so clear_focus (which protects the
884
+ # clicked swatch window closure) leaves the popover open when you click
885
+ # the tuple, and dismisses it when you click anywhere else.
886
+ # Flip up when opening down would run past the display bottom: the
887
+ # popover renders at the invoking cursor + window_pos (core_render's
888
+ # nested-window anchor), and the cursor here sits just under the swatch -
889
+ # so the up offset is the picker's own height plus the swatch row.
890
+ _pop_y = color_picker_top_offset()
891
+ _anchor_y = imgui.get_cursor_screen_pos()[1]
892
+ _disp_h = imgui.get_io().display_size[1]
893
+ if _anchor_y + _pop_y + picker_h > _disp_h - 10:
894
+ _pop_y = -(picker_h + 38)
895
+ color_changed, new_color = draw_color_picker(input_value, name=f"color_picker{unique}",
896
+ closed=not is_open, window_pos=(0, _pop_y), info=_info,
897
+ parent_window=draw_state, width=color_picker_width(), height=picker_h,
898
+ mode=Modes.POPOVER)
899
+ if is_open:
900
+ if color_changed:
901
+ if new_color is not None:
902
+ input_value = tuple(new_color)
903
+ else:
904
+ input_value = None
905
+ request_render()
906
+ changed = True
907
+ # Dismiss on a click outside the swatch/popover, or on Esc.
908
+ # if imgui.is_mouse_clicked(0):
909
+ # mx, my = imgui.get_mouse_pos()
910
+ # if not any(_is_in_subtree(d, draw_state) for d in Core.melty.bvh_query(mx, my)):
911
+ # Melty.popover_focused_ds = None
912
+ # request_render()
913
+ if any(k == glfw.KEY_ESCAPE for k, _ in Core.melty.frame_key_events):
914
+ Melty.popover_focused_ds = None
915
+ request_render()
916
+ # Keep re-rendering while a slider/square is being dragged so the live
917
+ # imgui interaction (is_item_active) updates the frame.
918
+ if Melty.imgui_any_item_active or imgui.is_mouse_down(0):
919
+ Melty.cache.invalidate_up(draw_state._tile_id, max_depth=10, force=True)
920
+ request_render()
921
+
922
+ # elif input_value is not None and len(input_value) > 0 and isinstance(input_value[0], (float, int)):
923
+ # str_value = ", ".join([str(v) for v in input_value])
924
+ # ch, input_str = imgui.input_text("##tuple", str_value)
925
+ # if ch:
926
+ # try:
927
+ # new_tuple = eval(f"({input_str},)")
928
+ # if isinstance(new_tuple, tuple):
929
+ # input_value = new_tuple
930
+ # changed = True
931
+ # except Exception:
932
+ # pass
933
+ # else:
934
+ # changed, input_value = draw_collection(input_value=input_value)
935
+
936
+ return changed, input_value
937
+
938
+
939
+ def draw_tuple_fast(input_value, draw_state, view_id, x=None, y=None, size=17,
940
+ outline=False, info=None, priority_delta=4, setter=None,
941
+ swatch=None, view_owner=None):
942
+ """draw_tuple's colour chip for immediate-mode bodies (the code editor's
943
+ tab bar) — the fast_dock idea: no render_func / imgui widget per chip,
944
+ the swatch goes straight to the draw list and the click is a plain
945
+ `draw_state.on_action` sub on the chip's rect, so a bar of N tabs pays
946
+ N rects instead of N wrapper calls. The picker is the same
947
+ `draw_color_picker` POPOVER draw_tuple opens; several chips share one
948
+ draw_state, so the open one is `draw_state._tint_edit_key == view_id`
949
+ (beside Melty.popover_focused_ds, which names the draw_state). `x`/`y`
950
+ default to the current cursor screen position; the chip claims no
951
+ layout. Returns (changed, value) like draw_tuple. `setter(value)` is the
952
+ write the caller makes with a changed value — given, every change is
953
+ recorded on the undo stack (a SetterChange on the host's draw_state,
954
+ keyed by view_id) and undo/redo re-apply it through the setter, since
955
+ a chip has no wrapper of its own for Melty.undo_requests to land in.
956
+ `swatch` (an rgb(a) tuple) is what the chip PAINTS instead of the value
957
+ itself — a muted preview, e.g. the tint mixed toward its background —
958
+ while the picker still opens on, and edits, the real value.
959
+ `view_owner` is the VIEW draw_state whose paint the chip edits (a
960
+ window header's tint chip passes its host — not the `owner` flag below,
961
+ which is this chip owning the popover): given, the picker also carries
962
+ the view's `bg_offset` / `z_offset` rows (`draw_view_offsets_fast`,
963
+ written back through `view_owner.locate_<param>`) and, with
964
+ `Toggles.dynamic_styles`, the Residuals tab."""
965
+ from meltygui.core.windowing.glfw_utils import request_render
966
+ from meltygui.view.color_view import draw_color_picker
967
+ from meltygui.view.color_view import _popover_anchor
968
+ from meltygui.view.color_view import color_picker_height
969
+ from meltygui.view.color_view import color_picker_top_offset
970
+ from meltygui.view.color_view import color_picker_width
971
+ import meltygui.core.windowing.window_api as glfw
972
+
973
+ # [tint=(0.85, 0.75, 0.05)]
974
+ corner_radius = 4.0
975
+ # [tint=(0.85, 0.75, 0.05)]
976
+ outline_color = (1.0, 1.0, 1.0, 0.55)
977
+ # The checker under a translucent chip (draw_tuple's COLOR_PREVIEW_HALF).
978
+ checker_dark, checker_light = (0.25, 0.25, 0.25), (0.6, 0.6, 0.6)
979
+
980
+ changed = False
981
+ if x is None or y is None:
982
+ cx, cy = imgui.get_cursor_screen_pos()
983
+ x = cx if x is None else x
984
+ y = cy if y is None else y
985
+ rect = (x, y, x + size, y + size)
986
+ draw_list = imgui.get_window_draw_list()
987
+
988
+ is_color = (isinstance(input_value, tuple) and len(input_value) in (3, 4)
989
+ and all(isinstance(c, (float, int)) for c in input_value))
990
+ if not is_color:
991
+ # Missing tint: a hollow chip; a click stamps in an opaque black.
992
+ draw_list.add_rect(x, y, x + size, y + size,
993
+ pack_color(1.0, 1.0, 1.0, 0.35),
994
+ rounding=corner_radius)
995
+ if draw_state.on_action("left_mouse_down", view_id=view_id, rect=rect,
996
+ priority_delta=priority_delta) is not None:
997
+ request_render()
998
+ return True, (0.0, 0.0, 0.0, 1.0)
999
+ return False, input_value
1000
+
1001
+ shown = swatch if swatch is not None else input_value
1002
+ r, g, b = float(shown[0]), float(shown[1]), float(shown[2])
1003
+ alpha = float(shown[3]) if len(shown) == 4 else 1.0
1004
+ if alpha < 1.0:
1005
+ # Left half: colour over a checkerboard at its real alpha; right
1006
+ # half: the colour opaque - so transparency shows in the chip.
1007
+ half = x + size * 0.5
1008
+ draw_list.add_rect_filled(x, y, half, y + size,
1009
+ pack_color(*checker_dark, 1.0),
1010
+ rounding=corner_radius,
1011
+ flags=imgui.DRAW_ROUND_CORNERS_LEFT)
1012
+ cell = size * 0.5
1013
+ draw_list.add_rect_filled(x + cell * 0.5, y, half, y + cell * 0.5,
1014
+ pack_color(*checker_light, 1.0))
1015
+ draw_list.add_rect_filled(x, y + cell * 0.5, x + cell * 0.5, y + size,
1016
+ pack_color(*checker_light, 1.0))
1017
+ draw_list.add_rect_filled(x, y, half, y + size,
1018
+ pack_color(r, g, b, alpha),
1019
+ rounding=corner_radius,
1020
+ flags=imgui.DRAW_ROUND_CORNERS_LEFT)
1021
+ draw_list.add_rect_filled(half, y, x + size, y + size,
1022
+ pack_color(r, g, b, 1.0),
1023
+ rounding=corner_radius,
1024
+ flags=imgui.DRAW_ROUND_CORNERS_RIGHT)
1025
+ else:
1026
+ draw_list.add_rect_filled(x, y, x + size, y + size,
1027
+ pack_color(r, g, b, 1.0),
1028
+ rounding=corner_radius)
1029
+ if outline:
1030
+ draw_list.add_rect(x - 1.5, y - 1.5, x + size + 1.5, y + size + 1.5,
1031
+ pack_color(*outline_color),
1032
+ rounding=corner_radius)
1033
+
1034
+ # `owner`: this chip last opened the popover. It stays the owner past an
1035
+ # outside click / escape / re-run (which clear the slot) until its next
1036
+ # run, where it draws the picker once more with closed=True and lets go.
1037
+ # The window itself does not wait for that run: a Mode.POPOVER window is
1038
+ # discarded by end_frame as soon as the slot no longer names it or an
1039
+ # ancestor (Melty.popover_orphaned) - so a chip whose host stopped
1040
+ # running (tab switched away, tile served from the blit cache) leaves its
1041
+ # picker hanging on screen.
1042
+ # draw_tuple's lifecycle: the popover slot names a draw_state whose
1043
+ # ancestor closure is protected from the click-away clear_focus. In
1044
+ # draw_tuple that is the chip's OWN draw_state; here every chip shares
1045
+ # the host's (the whole editor is its closure — nothing outside the
1046
+ # picker could ever close it), so the slot holds the PICKER WINDOW's
1047
+ # draw_state (its own closure = the picker) once it exists, and the
1048
+ # host only for the opening frame (covered by the popover grace).
1049
+ owner = getattr(draw_state, "_tint_edit_key", None) == view_id
1050
+ picker_name = f"color_picker{view_id}"
1051
+ # The picker hangs off the HEADER row, so its parent_window is the
1052
+ # enclosing WINDOW (what draw_tuple's shared ds has), never the host
1053
+ # itself: a collapsed host reads abs_closed, and end_frame discards
1054
+ # any nested window under an abs_closed parent - the picker opened on
1055
+ # a collapsed item's header closes on its first open.
1056
+ anchor = _popover_anchor(draw_state)
1057
+ picker_ds = None
1058
+ if owner:
1059
+ for nested in Melty.root_draw_states.get(anchor.id, ()):
1060
+ if getattr(nested, "name", None) == picker_name:
1061
+ picker_ds = nested
1062
+ break
1063
+ is_open = owner and Melty.popover_focused_ds is not None and (
1064
+ Melty.popover_focused_ds is draw_state
1065
+ or Melty.popover_focused_ds is picker_ds)
1066
+ if draw_state.on_action("left_mouse_down", view_id=view_id, rect=rect,
1067
+ priority_delta=priority_delta) is not None:
1068
+ if is_open:
1069
+ Melty.popover_focused_ds = None
1070
+ else:
1071
+ Melty.popover_focused_ds = draw_state
1072
+ draw_state._tint_edit_key = view_id
1073
+ Melty._popover_open_frame = Melty.frame_count # grace the opening click
1074
+ owner = True
1075
+ is_open = not is_open
1076
+ draw_state.invalidate()
1077
+ request_render()
1078
+ if not owner:
1079
+ return False, input_value
1080
+
1081
+ # ---- the picker popover: drawn while owned, closed once not open ----
1082
+ from meltygui.core.cache.invalidation_tracker import Note
1083
+ _info = (info() if callable(info) else info) if is_open else None
1084
+ picker_h = color_picker_height(4, bool(_info), has_owner=view_owner is not None)
1085
+ # Anchor under the chip; flip up past the display bottom (draw_tuple).
1086
+ _pop_y = color_picker_top_offset()
1087
+ _disp_w, _disp_h = imgui.get_io().display_size
1088
+ if y + size + _pop_y + picker_h > _disp_h - 10:
1089
+ _pop_y = -(picker_h + 38)
1090
+ # Flip LEFT the same way: a chip at a row's right edge (the file
1091
+ # listing's tint picker) cannot open off the display's right side, so
1092
+ # anchor the picker off the chip's right edge instead of its left.
1093
+ _pop_x = 0
1094
+ picker_w = color_picker_width()
1095
+ residual_tab = (view_owner is not None and Toggles.dynamic_styles and picker_ds is not None
1096
+ and any(isinstance(state, ColorPickerState) and state.tab == "residuals"
1097
+ for state in picker_ds.misc.values()))
1098
+ if residual_tab:
1099
+ picker_w = max(picker_w, 520)
1100
+ picker_h = max(picker_h, 560)
1101
+ _pop_y = color_picker_top_offset()
1102
+ if y + size + _pop_y + picker_h > _disp_h - 10:
1103
+ _pop_y = -(picker_h + 38)
1104
+ if x + picker_w > _disp_w - 10:
1105
+ _pop_x = -(picker_w - size)
1106
+ imgui.set_cursor_screen_pos((x, y + size))
1107
+ color_changed, new_color, picker_ds = draw_color_picker(
1108
+ input_value, name=picker_name, closed=not is_open,
1109
+ window_pos=(_pop_x, _pop_y), info=_info, parent_window=anchor,
1110
+ owner=view_owner,
1111
+ width=picker_w, height=picker_h, mode=Modes.POPOVER,
1112
+ return_extras=True)
1113
+
1114
+ imgui.same_line(spacing=0)
1115
+ if is_open and Melty.popover_focused_ds is draw_state:
1116
+ # Hand the slot from the host to the picker window now that it is
1117
+ # drawn (same frame, after the opening grace) - the draw_state the
1118
+ # wrapper hands back, never a lookup that can miss: while the slot
1119
+ # held the HOST, every click inside the host's window (the whole
1120
+ # editor for a side-bar chip) protected it, and the picker could not
1121
+ # be dismissed. From here the window lives on the slot alone -
1122
+ # Melty.popover_orphaned discards it at end_frame once the slot
1123
+ # moves away, whether or not this chip ever runs again.
1124
+ Melty.popover_focused_ds = picker_ds
1125
+ if not is_open:
1126
+ draw_state._tint_edit_key = None
1127
+ # The picker closed on the final frame: one DEEP cascade so every
1128
+ # nested tile under the host (depth-shifted bgs, headers, rows)
1129
+ # settles on it - the live edits below only reached the host's
1130
+ # direct children.
1131
+ Melty.cache.invalidate_up(draw_state._tile_id, force=True,
1132
+ max_depth=Toggles.Style.tint_edit_close_depth,
1133
+ note=Note(name="tint chip closed", reason=str(view_id),
1134
+ tint=(0.85, 0.75, 0.05)))
1135
+ request_render()
1136
+ return False, input_value
1137
+ if color_changed:
1138
+ previous = input_value
1139
+ from meltygui.core.styling.style import Style
1140
+ input_value = (Style(new_color, **previous.__getnewargs_ex__()[1])
1141
+ if isinstance(previous, Style) and new_color is not None
1142
+ else tuple(new_color) if new_color is not None else None)
1143
+ changed = True
1144
+ if setter is not None:
1145
+ from meltygui.state.core_undo import UndoManager
1146
+ UndoManager.record(draw_state, previous, input_value, setter=setter,
1147
+ key=view_id, label=str(view_id))
1148
+ # A live edit: the host (the bg its tint paints) and its direct
1149
+ # children re-render - a shallow cascade, because the picker fires
1150
+ # this every frame of a drag. The host's OWN tile is what
1151
+ # `draw_state.invalidate()` covered; its children are what it
1152
+ # missed (the tint shows through them). Deeper tiles wait for the
1153
+ # closing cascade above.
1154
+ Melty.cache.invalidate_up(draw_state._tile_id, force=True,
1155
+ max_depth=Toggles.Style.tint_edit_live_depth,
1156
+ note=Note(name="tint chip edit", reason=str(view_id),
1157
+ tint=(0.85, 0.75, 0.05)))
1158
+ request_render()
1159
+ if any(k == glfw.KEY_ESCAPE for k, _ in Core.melty.frame_key_events):
1160
+ Melty.popover_focused_ds = None
1161
+ request_render()
1162
+ # Keep the frames coming while a slider/square drag is live - the
1163
+ # picker is use_cache=False, so a frame is all it needs; the host's
1164
+ # tiles are triggered by the change branch above, never a frame.
1165
+ if Melty.imgui_any_item_active or imgui.is_mouse_down(0):
1166
+ request_render()
1167
+ return changed, input_value
1168
+
1169
+
1170
+ @render_func(is_default_for=(types.MappingProxyType), shadow=False, show_bg=False, show_add_delete=False,
1171
+ with_header=draw_header)
1172
+ def draw_mapping_proxy(input_value):
1173
+ # To list first, then back to mapping proxy
1174
+ try:
1175
+ dict_values = dict(input_value)
1176
+ changed, new_dict = draw_collection(dict_values, show_bg=False, indent_size=0, show_header=False,
1177
+ show_add_delete=False)
1178
+ if changed:
1179
+ return True, types.MappingProxyType(new_dict)
1180
+
1181
+ except Exception as e:
1182
+ imgui.text(f"Error converting MappingProxyType to dict: {e}")
1183
+ return False, input_value
1184
+
1185
+ return changed, input_value