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,1082 @@
1
+ """Dropdown view functions and supporting definitions."""
2
+ from meltygui.core.runtime.paths import debug_log_path
3
+ from meltygui.core.runtime.toggles import Toggles
4
+ from meltygui.hdr_color import pack_color
5
+ from meltygui.core.melty import Melty
6
+ from meltygui.core.core_render import render_func
7
+ from meltygui.core.rendering.core_decoration import Core
8
+ from meltygui.core.rendering.window_decoration import window
9
+ from meltygui.state.new_core_model import DropDownState
10
+ from meltygui.core.runtime.toggles import Tint
11
+ from meltygui.view.header_view import draw_header
12
+ from meltygui.view.header_view import flat_button
13
+ import meltygui_imgui as imgui
14
+ from meltygui.core.core_render import SCROLLBAR_MARGIN
15
+ from meltygui.core.core_render import SCROLL_BAR_WIDTH_DEFAULT
16
+ import traceback
17
+
18
+ # Inset of a left/right aligned trigger label from the trigger edge, in pixels.
19
+ # Raise it to move the caret and label further from the edge.
20
+ TRIGGER_TEXT_INSET = 5
21
+
22
+
23
+ @render_func(use_cache=True, temp=True)
24
+ def draw_drop_down_item(input_value, name="", unique=0, shadow=False, draw_state=None, **kwargs):
25
+ from meltygui.view.control_view import button
26
+
27
+ hovered = draw_state._bounding_hovered
28
+ clicked, _ = button(name, name=f"{unique}{name}_dd_item", show_bg=True, height=25,
29
+ shadow=False, hovered=hovered)
30
+
31
+ # Hover highlight: paint a translucent wash over this item's own box (its
32
+ # draw_state is the row, so abs_left/abs_top + width/height frame it exactly)
33
+ # straight onto the window draw list so it sits over the button fill.
34
+ if hovered:
35
+ dl = imgui.get_window_draw_list()
36
+ dl.add_rect_filled(draw_state.abs_left, draw_state.abs_top,
37
+ draw_state.abs_left + draw_state.width,
38
+ draw_state.abs_top + draw_state.height,
39
+ pack_color(1, 1, 1, 0.16),
40
+ rounding=getattr(draw_state, 'corner_radius', 6))
41
+
42
+ if clicked:
43
+ return True, name
44
+
45
+ return False, input_value
46
+
47
+
48
+ @render_func(use_cache=True, show_bg=False, selectable=False,
49
+ tint=(0.083, 0.10, 0.144),
50
+ is_tree=False, show_name=True, with_header=draw_header)
51
+ @window
52
+ def draw_dropdown(input_value, collection, name, draw_state, unique, drop_down_state: DropDownState, shadow=True,
53
+ text_align="left", open_upwards=None, menu_min_width=None, display_label=None, **kwargs):
54
+ """Root of a recursive dropdown. Renders a trigger button showing the current
55
+ selection; clicking it opens the (click-o-open) root popover. Nested dict
56
+ rows inside the popover open their own sub-menus on hover. Returns
57
+ (changed, selected_leaf) when the user picks a value. `open_upwards`
58
+ defaults to choosing the side with room. True forces above, False below;
59
+ `menu_min_width` sets its width floor.
60
+
61
+ Open/closed is a single global slot -- ``Melty.popover_focused_ds`` holds the
62
+ draw_state of whichever dropdown's popover is currently shown. Each dropdown
63
+ decides it's open by identity (``popover_focused_ds is draw_state``), so
64
+ opening one popover implicitly closes every other (they all fail the test).
65
+ ``drop_down_state`` is the per-view scratch object the framework re-injects
66
+ every frame; we stash the last picked leaf on it for the trigger label."""
67
+ from meltygui.core.conversion.cache_tree import UNSET_VALUE
68
+ from meltygui.core.windowing.glfw_utils import request_render
69
+ from meltygui.model.dropdown_model import _dd_as_tuple
70
+ from meltygui.core.layout.dropdown_core import _dd_close
71
+ from meltygui.core.layout.dropdown_core import _dd_handle_keys
72
+ from meltygui.model.dropdown_model import _dd_label_for_path
73
+ from meltygui.model.dropdown_model import _dd_path_for_value
74
+ from meltygui.model.dropdown_model import _dd_walk
75
+ import meltygui.core.windowing.window_api as glfw
76
+
77
+ from meltygui.core.rendering.mode import Mode
78
+
79
+ # DEBUG
80
+ # Is THIS dropdown the one whose popover is showing?
81
+ is_open = Melty.popover_focused_ds is draw_state
82
+ _DD_DBG = False # TEMP: default False for dropdown-close investigation
83
+ if _DD_DBG:
84
+ _pf = Melty.popover_focused_ds
85
+ if is_open or _pf is not None:
86
+ try:
87
+ _mx, _my = imgui.get_mouse_pos()
88
+ _clk = imgui.is_mouse_clicked(0);
89
+ _dn = imgui.is_mouse_down(0)
90
+ except Exception:
91
+ _mx = _my = -1.0;
92
+ _clk = _dn = None
93
+ with open(debug_log_path("dd_debug.log"), "a") as _f:
94
+ _f.write(f"[DD-DBG] f={Melty.frame_count} name={name!r} ds={id(draw_state)} "
95
+ f"pf={id(_pf) if _pf is not None else None} is_open={is_open} "
96
+ f"mouse=({_mx:.0f},{_my:.0f}) clk={_clk} down={_dn} "
97
+ f"sel_path={getattr(drop_down_state, 'selected_path', ())} "
98
+ f"open_frame={getattr(Melty, '_popover_open_frame', None)}\n")
99
+
100
+ # Label shows the last pick (sticky across frames via drop_down_state),
101
+ # falling back to the raw input value.
102
+ # Title shows the LABEL/key of the current selection (e.g. "red"), not the raw
103
+ # value (which may be a tuple/number); selected_label is stamped at pick-time.
104
+ # The caller's input_value is the selection of record: when that MOVES
105
+ # (the caller adopted a pick, a nav-undo replay, a selection carried
106
+ # over from elsewhere), the text follows it - the label/path of the
107
+ # leaf holding that value - instead of whatever selected_label was last
108
+ # stamped. The stamp stays put while input_value holds still (callers
109
+ # passing a static value rely on the last pick showing). Callers used to
110
+ # re-stamp selected_label themselves after the draw: on the pick frame
111
+ # their input was still the OLD value, so the old label went back into
112
+ # the state, and with this body frozen the wrong text sat until a
113
+ # hover repaint. Arriving here runs on the frame the input changed -
114
+ # which is a frame before body runs (the input is the tile's hash).
115
+ if input_value != getattr(drop_down_state, "_dd_last_input", UNSET_VALUE):
116
+ drop_down_state._dd_last_input = input_value
117
+ _in_path = _dd_path_for_value(collection, input_value)
118
+ if _in_path is not None:
119
+ drop_down_state.selected_path = _in_path
120
+ drop_down_state.selected_label = _dd_label_for_path(collection, _in_path)
121
+ _sel_label = getattr(drop_down_state, "selected_label", "") or ""
122
+ current = display_label if display_label is not None else (_sel_label if _sel_label else (str(input_value) if input_value is not None else ""))
123
+ caret = "" if is_open else "" # fa-chevron-down / fa-chevron-right
124
+
125
+ # Compact mode: in a very narrow slot (e.g. an inline table cell) there's no room
126
+ # for the caret + button chrome, so show NOTHING but the selected value. Still a
127
+ # real (bg-less) button, so it stays clickable to open the popover. Threshold is
128
+ # tunable via compact_below (px).
129
+ #
130
+ # The trigger's slot width: an explicit caller `width` wins, only without one
131
+ # do we fall back to the measured content_width. Measurement must never feed
132
+ # back into the trigger size - an open popover inflates content_width, which
133
+ # would flip compact mode off and balloon the trigger (~240px), wrapping the
134
+ # header row it sits in: the disagreement between the drawn size and the
135
+ # measured item rect is exactly what reads as animation jitter.
136
+ _slot_w = kwargs.get("width") or draw_state.content_width
137
+ compact = False
138
+
139
+ # A compact trigger hugs its glyph: minimal text pad and a centered label,
140
+ # so a small chevron/icon cell doesn't balloon to full text label width.
141
+ trigger_pad = kwargs.get("text_pad", 6 if compact else 15)
142
+ trigger_align = "center" if compact else text_align
143
+ trigger_w = max(15 if compact else 18, _slot_w)
144
+
145
+ # The label must FIT the fixed trigger width in pixels - a long label
146
+ # overflows the button rect and the measured item rect disagrees with
147
+ # the drawn size, the same disagreement the width comment above calls
148
+ # out as height jitter. Ellipsis-trim via a text width, not a
149
+ # character count (glyph widths vary wildly with font/monospace).
150
+ _label_px = max(4.0, trigger_w - trigger_pad * 2)
151
+ if compact:
152
+ # Show the VALUE itself (not the label/key). For a name->glyph dropdown
153
+ # the trigger must hug the glyph, not become the picked value's name.
154
+ drop_down_display_str = _dd_fit_label(
155
+ str(input_value if input_value is not None else current), _label_px)
156
+ else:
157
+ drop_down_display_str = _dd_fit_label(f"{caret} {str(current)}", _label_px)
158
+
159
+ # A caller's trigger_height taller than the 25 px slot also moves the popover anchor down.
160
+ trigger_h = (getattr(draw_state, "content_height", 0) or 25) if compact else max(25, kwargs.get("trigger_height", 25))
161
+ # Quiet text dimming (text_toward_bg - the info tab's info rows): a
162
+ # selection WITHOUT an embedded tint fades toward the background, so a
163
+ # tinted one (the active source in yellow) is what draws the eye.
164
+ _ttb = kwargs.get("text_toward_bg", 0.0)
165
+ trigger_text_value = 1.023
166
+ if _ttb and getattr(input_value, "tint", None) is None:
167
+ trigger_text_value = 1.023 * (1.0 - min(max(float(_ttb), 0.0), 1.0))
168
+ trigger_left, trigger_top = imgui.get_cursor_screen_pos()
169
+ # Draw-list trigger: the click is claimed through this dropdown's own
170
+ # draw_state (no nested render_func). It fires on press, as the old button did.
171
+ # An embedded tint colours the trigger; otherwise flat_button's default hue applies.
172
+ embedded_tint = _dd_obj_tint(input_value)
173
+ trigger_color = {} if embedded_tint is None else {"color": embedded_tint}
174
+ label_width = imgui.calc_text_size(drop_down_display_str.split("##")[0]).x
175
+ if trigger_align == "left":
176
+ trigger_text_offset = TRIGGER_TEXT_INSET
177
+ elif trigger_align == "right":
178
+ trigger_text_offset = trigger_w - label_width - TRIGGER_TEXT_INSET
179
+ else:
180
+ trigger_text_offset = None
181
+ clicked = flat_button(drop_down_display_str, draw_state, view_id=f"{name}_dd_trigger{unique}",
182
+ width=trigger_w, height=trigger_h,
183
+ alpha=1.0 if kwargs.get("show_button_bg", True) else 0.0,
184
+ shadow=shadow, text_value=trigger_text_value, text_pad=trigger_pad,
185
+ text_offset_x=trigger_text_offset, event="left_mouse_down", **trigger_color)
186
+
187
+ if clicked:
188
+
189
+ was_open = is_open
190
+ if _DD_DBG:
191
+ with open(debug_log_path("dd_debug.log"), "a") as _f:
192
+ _f.write(f"[DD-DBG] CLICK toggle f={Melty.frame_count} name={name!r} was_open={was_open}\n")
193
+ Melty.popover_focused_ds = None if is_open else draw_state
194
+ is_open = Melty.popover_focused_ds is draw_state
195
+ if is_open and not was_open:
196
+ Melty._popover_open_frame = Melty.frame_count # grace the opening click
197
+ # Fresh open: start with an empty query and give the search box a few
198
+ # frames to grab text focus so the user can type to filter immediately.
199
+ drop_down_state.search_query = ""
200
+ drop_down_state.search = ""
201
+ drop_down_state._focus_search = 8 if len(collection) > 4 else 0
202
+ if len(collection) <= 4:
203
+ Melty.clear_focus(not_this=draw_state)
204
+ # Start the highlight on the last-selected item (expanded to it) rather
205
+ # than the top, so re-opening starts where you left off.
206
+ _sp = _dd_as_tuple(getattr(drop_down_state, "selected_path", ()))
207
+ drop_down_state.cursor_path = _sp
208
+ drop_down_state.open_path = _sp[:-1] if _sp else ()
209
+ drop_down_state._kbd_mode = True
210
+ drop_down_state._last_mouse = None
211
+ drop_down_state._had_focus = False
212
+ if not is_open:
213
+ _dd_close(drop_down_state)
214
+ draw_state.invalidate()
215
+
216
+ request_render()
217
+
218
+ # The popover is a latching window -- the first call registers it and it
219
+ # stays alive, so we always call it and toggle visibility with `closed`
220
+ # rather than skipping the call (skipping would leave the last-open frame
221
+ # painted). closed=True hides the whole subtree. window_pos pins it just
222
+ # under the trigger (relative to this dropdown window) so it doesn't drift
223
+ # off as a free-floating draggable; temp keeps it ephemeral. root_state
224
+ # carries the single open-path the recursion expands; path_prefix starts
225
+ # empty at the root. A pick bubbles back as (changed, value).
226
+ if is_open:
227
+ # A mouse move switches back to hover mode so the highlight follows the
228
+ # pointer again (until the next arrow key locks keyboard mode).
229
+ _mp = imgui.get_mouse_pos()
230
+ _lm = getattr(drop_down_state, "_last_mouse", None)
231
+ if _lm is not None and (abs(_mp[0] - _lm[0]) > 0.5 or abs(_mp[1] - _lm[1]) > 0.5):
232
+ drop_down_state._kbd_mode = False
233
+ drop_down_state._last_mouse = (_mp[0], _mp[1])
234
+
235
+ # Size from the current rows, never the previous window's clipped bounds.
236
+ # The model catalog may arrive after opening, and a search may temporarily
237
+ # leave the items. Neither may become the next menu's permanent size.
238
+ menu_width, menu_height, menu_top = _dd_popup_geometry(
239
+ collection, getattr(drop_down_state, "search_query", ""),
240
+ trigger_top, trigger_h, open_upwards, max(trigger_w, menu_min_width or 0))
241
+ menu_left = max(0, min(trigger_left, imgui.get_io().display_size[0] - menu_width))
242
+ changed, new_item, menu_ds = draw_dd_menu(
243
+ collection, tint=draw_state.tint,
244
+ name=f"{unique}_menu",
245
+ closed=not is_open, temp=True, shadow=False, auto_resize=False,
246
+ window_pos=(menu_left - imgui.get_cursor_screen_pos()[0], menu_top - imgui.get_cursor_screen_pos()[1]),
247
+ width=menu_width, height=menu_height,
248
+ parent_window=draw_state, swoosh=False, disable_scroll=False,
249
+ row_tags=kwargs.get("row_tags"),
250
+ row_tints=kwargs.get("row_tints"),
251
+ row_actions=kwargs.get("row_actions"),
252
+ text_toward_bg=kwargs.get("text_toward_bg", 0.0),
253
+ root_state=drop_down_state, path_prefix=(), return_extras=True)
254
+ drop_down_state._menu_ds = menu_ds
255
+ if is_open:
256
+ pending = getattr(drop_down_state, "_pending_pick", None)
257
+ if pending is not None:
258
+ drop_down_state._pending_pick = None
259
+ drop_down_state._picked_path = _dd_as_tuple(pending)
260
+ changed, new_item = True, _dd_walk(collection, _dd_as_tuple(pending))
261
+ if changed:
262
+ _p = _dd_as_tuple(getattr(drop_down_state, "_picked_path", ()))
263
+ drop_down_state.selected_path = _p
264
+ drop_down_state.selected_label = _dd_label_for_path(collection, _p)
265
+ if _DD_DBG:
266
+ with open(debug_log_path("dd_debug.log"), "a") as _f:
267
+ _f.write(f"[DD-DBG] CLOSE via menu-pick f={Melty.frame_count} name={name!r} picked={_p}\n")
268
+ Melty.popover_focused_ds = None # picking dismisses the popover
269
+ _dd_close(drop_down_state)
270
+ draw_state.invalidate()
271
+ request_render()
272
+ return True, new_item
273
+
274
+ # The dropdown owns the keyboard while open: its search box holds text
275
+ # focus (taken on open, released on close), so OTHER text editors gate off
276
+ # (they all check Melty.text_focused_ds) and never act on the same keys. We
277
+ # only handle nav keys while we actually hold that focus - that's what
278
+ # keeps the arrow/Enter/Esc collisions with whatever editor was active.
279
+ box_tile = getattr(drop_down_state, "_search_box_tile", None)
280
+ text_focused = (Melty.text_focused_ds is not None and box_tile is not None
281
+ and getattr(Melty.text_focused_ds, "_tile_id", None) == box_tile)
282
+ if len(collection) <= 4:
283
+ text_focused = True
284
+
285
+ # Esc dismisses the open dropdown (and releases its text focus via
286
+ # _dd_close). Ungated: the global text box Esc handler may have already
287
+ # cleared the box's focus this same frame, so we don't require it here.
288
+ if any(k == glfw.KEY_ESCAPE for k, _ in Core.melty.frame_key_events):
289
+ if _DD_DBG:
290
+ with open(debug_log_path("dd_debug.log"), "a") as _f:
291
+ _f.write(f"[DD-DBG] CLOSE via Esc f={Melty.frame_count} name={name!r}\n")
292
+ Melty.popover_focused_ds = None
293
+ _dd_close(drop_down_state)
294
+ draw_state.invalidate()
295
+ request_render()
296
+ return False, input_value
297
+
298
+ # Arrows / Enter only while we own the keyboard, so they don't also drive
299
+ # whatever editor was active when the dropdown opened.
300
+ if text_focused:
301
+ search = getattr(drop_down_state, "search", "") or ""
302
+ picked = _dd_handle_keys(collection, drop_down_state, search=search,
303
+ text_focused=text_focused)
304
+ if picked is not UNSET_VALUE:
305
+ _p = _dd_as_tuple(getattr(drop_down_state, "_picked_path", ()))
306
+ drop_down_state.selected_path = _p
307
+ drop_down_state.selected_label = _dd_label_for_path(collection, _p)
308
+ Melty.popover_focused_ds = None
309
+ _dd_close(drop_down_state)
310
+ draw_state.invalidate()
311
+ request_render()
312
+ return True, picked
313
+
314
+ # Window press dispatch owns click-away dismissal. It uses the captured
315
+ # press position and both parent links; a second post-imgui check here
316
+ # races deferred menu rendering and mistakes nested menu clicks for exits.
317
+
318
+ # Focus settle (bounded, NOT a permanent repaint loop): right after open
319
+ # the search box asks for text focus, but the opening click's
320
+ # request_focus can race it the same frame. While the box hasn't
321
+ # confirmed focus and we're still within the small retry budget, re-run
322
+ # so it asks again; it gets within a frame or two and this stops.
323
+ # Steady-state open repaints nothing - mouse changes invalidate via
324
+ # _dd_set_cursor, keys via begin_frame. The retry must invalidate the
325
+ # BOX's own chain (invalidate_up from its tile): the menu is a separate
326
+ # cached window subtree, so invalidating just this trigger view never
327
+ # re-rendered the box on a reopen and request_focus never re-fired.
328
+ if getattr(drop_down_state, "_focus_search", 0) > 0:
329
+ box_tile = getattr(drop_down_state, "_search_box_tile", None)
330
+ if box_tile is not None:
331
+ Melty.cache.invalidate_up(box_tile, force=True)
332
+ Melty.cache.invalidate(draw_state._tile_id, force=True)
333
+ request_render()
334
+
335
+ return False, input_value
336
+
337
+
338
+ @render_func(use_cache=True, show_bg=True, shadow=True, selectable=False, temp=True,
339
+ closable=True, popover=True, melty_window=False, auto_resize=True, with_header=None,
340
+ max_height=420, min_width=300, swoosh=False, min_height=33, keep_in_view=True)
341
+ def draw_dd_menu(input_value, draw_state, root_state=None, unique=0, path_prefix=(), tint=None,
342
+ show_search=None, text_align="right", row_tags=None, row_tints=None,
343
+ row_suffixes=None, row_actions=None, text_toward_bg=0.0,
344
+ full_render=False, row_code=None, **kwargs):
345
+ """One level of the dropdown, drawn as its own temp popover window. Iterates
346
+ the level's entries and renders each as a row (`_dd_menu_row`); a leaf click
347
+ or a pick inside a nested sub-menu bubbles back up as (changed, value).
348
+
349
+ Expansion is driven by `root_state.open_path` (a single chain of keys, see
350
+ DropDownState) — NOT by each row testing its own hover. A row renders its
351
+ sub-menu only when `open_path` runs through it, so at most one sub-menu is
352
+ open per level and hidden siblings can never resurface. `path_prefix` is this
353
+ level's key chain from the root; each row's full path is prefix + its key.
354
+
355
+ `show_search` draws the root-level filter box. Autocomplete (the code editor's
356
+ suggestion popup) passes False: the editor itself owns text focus and the
357
+ half-typed identifier IS the filter, so a second focus-stealing search box
358
+ would fight it. The caller pre-filters the rows in that case.
359
+
360
+ The popover and its rows are CACHED tiles with NO kwargs cache key —
361
+ a clean row tile blit-skips even when its cursor/open path inputs changed.
362
+ Repaints are therefore driven by explicit invalidation only: an open_path
363
+ change cascades invalidate_up from the menu tile inside _dd_set_cursor
364
+ (this is what re-runs a stale branch row so it stamps its submenu window
365
+ closed — see the leak note there), keys via the begin_frame popover hook
366
+ (or the code editor's per-event invalidate_up) — invalidate_up
367
+ specifically, since it cascades to the row tiles; a plain invalidate
368
+ leaves the inner dd_rows collection clean and it blit-skips."""
369
+ from meltygui.core.conversion.cache_tree import UNSET_VALUE
370
+ from meltygui.core.styling.fonts import Font
371
+ from meltygui.core.windowing.glfw_utils import request_render
372
+ from meltygui.view.text_view import draw_text
373
+ from meltygui.model.dropdown_model import _dd_as_tuple
374
+ from meltygui.model.dropdown_model import _dd_first_match_leaf
375
+ from meltygui.model.dropdown_model import _dd_row_lookup
376
+ from meltygui.core.layout.dropdown_core import _dd_set_cursor
377
+ from meltygui.model.dropdown_model import _dd_visible_entries
378
+
379
+ if show_search is None:
380
+ show_search = len(input_value) > 4
381
+ if show_search and not path_prefix and root_state is not None:
382
+ # Root owns the search box. Single-line so Up/Down/Enter pass through to
383
+ # menu nav; it auto-focuses once when the menu opens (_focus_search).
384
+ q = getattr(root_state, "search_query", "") or ""
385
+ box = draw_text(q, name=f"dd_search{unique}", show_name=False, searchable=False,
386
+ single_line=True, is_search_box=True, is_tree=False,
387
+ with_header=None, with_footer=None, show_bg=True, shadow=False,
388
+ request_focus=getattr(root_state, "_focus_search", 0) > 0,
389
+ font=Font.JETBRAINS_MONO_19,
390
+ tint=tint, return_extras=True)
391
+ q_changed, new_q = box[0], box[1]
392
+ box_ds = box[2] if len(box) > 2 else None
393
+ if box_ds is not None:
394
+ root_state._search_box_tile = box_ds._tile_id
395
+ # Focus retry is a bounded countdown: the first popover's
396
+ # move-to-front clears text focus the frame the box first grabs it
397
+ # (apply_move_to_front(): parent != front window), so a one-shot
398
+ # request is lost. Re-request for a few frames until it lands, then
399
+ # clear (0). Draw_dropdown drives the re-runs while this is > 0.
400
+ if Melty.text_focused_ds is box_ds:
401
+ root_state._focus_search = 0
402
+ elif getattr(root_state, "_focus_search", 0) > 0:
403
+ root_state._focus_search -= 1
404
+ new_search = str(new_q or "").strip().lower()
405
+ if q_changed:
406
+ root_state.search_query = new_q
407
+ # Jump the cursor onto the first matching leaf, auto-expanding all
408
+ # branches above it, so the leaf is visible and one Enter selects it
409
+ # (instead of Enter-to-open-then-Enter-to-pick).
410
+ if new_search:
411
+ leaf = _dd_first_match_leaf(input_value, new_search)
412
+ if leaf is not None:
413
+ _dd_set_cursor(root_state, leaf, False, menu_ds=draw_state)
414
+ else:
415
+ _dd_set_cursor(root_state, (), False, menu_ds=draw_state)
416
+ else:
417
+ _dd_set_cursor(root_state, (), False, menu_ds=draw_state)
418
+ root_state.search = new_search
419
+
420
+ search = str(getattr(root_state, "search", "") or "")
421
+ # Cursor/open paths are passed down as ROW INPUTS (not read by the row's
422
+ # root_state): a row is a use_cache=True tile, so its highlight only repaints
423
+ # when an input changes. Throwing the paths through makes keyboard nav and the
424
+ # default-selection-on-open repaint (hover repaints via a separate hook).
425
+ open_path = _dd_as_tuple(getattr(root_state, "open_path", ()))
426
+ cursor_path = _dd_as_tuple(getattr(root_state, "cursor_path", ()))
427
+
428
+ # Stale-submenu sweep (root level only - the registry is keyed by full row
429
+ # path, so one sweep covers every depth): a branch row stamps its submenu
430
+ # `closed` only when the row body runs, and an active search can filter
431
+ # the row itself out of `rows` while its submenu is open - the submenu
432
+ # window then floats on with nothing left to close it. dd_menu_row
433
+ # registers its submenu draw_state in root_state._dd_submenu_ds; the
434
+ # root body re-runs on every query/open_path change, so close anything
435
+ # here that's no longer in the open path. The row re-registers/reopens it
436
+ # if it ever comes back on-path.
437
+ if not path_prefix and root_state is not None:
438
+ for _sp, _sds in list(getattr(root_state, "_dd_submenu_ds", {}).items()):
439
+ if (_sds is not None and open_path[:len(_sp)] != _sp
440
+ and not getattr(_sds, "closed", True)):
441
+ _sds.closed = True
442
+ request_render()
443
+
444
+ ancestor_matched = bool(search) and any(search in str(k).lower() for k in path_prefix)
445
+ rows = _dd_visible_entries(input_value, "" if ancestor_matched else search)
446
+ # if not rows and search:
447
+ # imgui.dummy(180, 6)
448
+ # text(" No matches", width=180, height=_DD_ROW_H, name="dd_nomatch",
449
+ # text_colour=(1, 1, 1))
450
+ # return False, None
451
+
452
+ # Per-row kwargs shared by both render paths. Each row is a
453
+ # (key, value, label, is_branch) tuple handed to dd_menu_row, which computes
454
+ # its own path/cursor/open state from path_prefix + root_state. full_render is
455
+ # threaded down so nested sub-menus inherit the same render path.
456
+ row_kwargs = dict(show_bg=False, shadow=False, path_prefix=tuple(path_prefix),
457
+ root_state=root_state, tint=tint, text_align=text_align, z_offset=0,
458
+ row_tags=row_tags, row_tints=row_tints, row_suffixes=row_suffixes,
459
+ cursor_path=cursor_path,
460
+ open_path=open_path, full_render=full_render)
461
+
462
+ # Manual row loop (the dd_collection full_render path is gone - its
463
+ # per-row render_func tiles cost more than they saved; virtualization is
464
+ # done directly below instead). Branch rows still go through the
465
+ # dd_menu_row render_func (they own a nested submenu + a real draw_state);
466
+ # leaf rows - the bulk of a big list - are drawn inline by _dd_leaf_row
467
+ # with raw imgui, skipping the per-row wrapper overhead that made long
468
+ # menus crawl while interacting.
469
+ result = (False, input_value)
470
+
471
+ # Viewport culling for big flat menus (the 967-icon picker): rows are a
472
+ # fixed _DD_ROW_H pitch, so a leaf row scrolled outside the window band
473
+ # skips its draw entirely and reserves the space with a dummy. The width
474
+ # is the cached widest label width so the auto_resize width doesn't
475
+ # jitter with the visible set (the cache recomputes once per search
476
+ # change, not per frame). Branch rows are never culled: a parent stamps
477
+ # `closed` on their submenu window each run (see the leak note in
478
+ # _dd_set_cursor). Gated on a known height - the first frame draws
479
+ # everything once - and on row count so small menus keep the simple path.
480
+ _cull_top = _cull_bot = None
481
+ if root_state is not None and len(rows) >= 40 and draw_state.height:
482
+ _wt = draw_state._abs_top()
483
+ _cull_top, _cull_bot = _wt, _wt + draw_state.height
484
+ _wkey = (len(rows), search)
485
+ if getattr(root_state, "_row_w_key", None) != _wkey:
486
+ _mw = 0.0
487
+ for (_k, _v, _lbl, _b) in rows:
488
+ _sfx = _dd_row_lookup(row_suffixes, _v) if row_suffixes else None
489
+ _tag = _dd_row_lookup(row_tags, _v) if row_tags else None
490
+ _mw = max(_mw, imgui.calc_text_size(str(_lbl) + (_sfx or ""))[0]
491
+ + (_dd_tag_width(_tag) + 16.0 if _tag else 0.0))
492
+ root_state._row_w_key = _wkey
493
+ root_state._row_w = _mw + 24.0
494
+ # Shared label column for code rows: every row's code preview starts at
495
+ # the same x (the widest label, capped - a long scope name does not eat
496
+ # the code's width), so the embedded editors are up line to line -
497
+ # per-row label widths made each row's code start jagged.
498
+ code_label_w = None
499
+ if row_code:
500
+ _ws = [imgui.calc_text_size(str(_l).rstrip())[0]
501
+ for (_k, _v, _l, _b) in rows
502
+ if not _b and _dd_row_lookup(row_code, _v) is not None]
503
+ if _ws:
504
+ code_label_w = min(max(_ws), Toggles.Dropdown.code_label_max_width)
505
+ row_width = _dd_row_width(draw_state)
506
+ for idx, row in enumerate(rows):
507
+ key, value, label, is_branch = row
508
+ if is_branch:
509
+ changed, picked = dd_menu_row(row, name=f"ddrow_{idx}_{key}", **row_kwargs)
510
+ if changed:
511
+ result = (True, picked)
512
+ else:
513
+ if _cull_top is not None:
514
+ _cx0, _cy0 = imgui.get_cursor_screen_pos()
515
+ if _cy0 + Toggles.Dropdown.row_height <= _cull_top or _cy0 >= _cull_bot:
516
+ # Offscreen leaf: reserve its draw rect (width from the
517
+ # cached widest label so measure/auto-resize stay stable)
518
+ # and pin the cursor a row down, like a hidden row does.
519
+ imgui.dummy(getattr(root_state, "_row_w", 1.0), Toggles.Dropdown.row_height)
520
+ imgui.set_cursor_screen_pos((_cx0, _cy0 + Toggles.Dropdown.row_height))
521
+ continue
522
+ picked = _dd_leaf_row(key, value, label, draw_state, root_state,
523
+ tuple(path_prefix), cursor_path, tint=tint,
524
+ row_tags=row_tags, row_tints=row_tints,
525
+ row_suffixes=row_suffixes,
526
+ row_actions=row_actions,
527
+ text_toward_bg=text_toward_bg,
528
+ row_code=row_code,
529
+ code_label_w=code_label_w,
530
+ row_width=row_width)
531
+ if picked is not UNSET_VALUE:
532
+ result = (True, picked)
533
+ return result
534
+
535
+
536
+ @render_func(use_cache=True, show_bg=False, shadow=False, selectable=False, temp=True, show_add_delete=False,
537
+ with_header=None, disable_scroll=True, min_width=300, swoosh=False, z_offset=-3)
538
+ def dd_menu_row(input_value, draw_state, text_align="right", path_prefix=(),
539
+ root_state=None, tint=None, row_tags=None, row_tints=None,
540
+ cursor_path=(), open_path=(), full_render=True, **kwargs):
541
+ """A single BRANCH menu row (leaves go through the raw _dd_leaf_row).
542
+ `input_value` is the row TUPLE (key, value, label, is_branch) handed in by
543
+ draw_dd_menu's manual loop. `cursor_path` / `open_path` are
544
+ passed IN (not read from root_state) so they're cache-key inputs: a row is a
545
+ use_cache=True tile, so its keyboard highlight / open chevron only repaint when
546
+ an input changes. Leaves are a button returning the VALUE on click; branch rows
547
+ show a chevron and own a nested `draw_dd_menu` to their right, shown only while
548
+ on the open path. Hovering points the shared cursor here; the highlight is
549
+ painted over the row box when hovered / keyboard-current.
550
+
551
+ `row_tags` (optional) maps a value -> short dim string drawn right-aligned —
552
+ the code editor's completion popup uses it for the kind label (func/class/…)."""
553
+ from meltygui.view.control_view import button
554
+ from meltygui.model.dropdown_model import _dd_as_tuple
555
+ from meltygui.core.layout.dropdown_core import _dd_invalidate_rows
556
+ from meltygui.core.layout.dropdown_core import _dd_pick
557
+ from meltygui.model.dropdown_model import _dd_row_lookup
558
+ from meltygui.core.layout.dropdown_core import _dd_set_cursor
559
+
560
+ key, value, label, is_branch = input_value
561
+ row_path = tuple(path_prefix) + (key,)
562
+ open_path = _dd_as_tuple(open_path)
563
+ cursor_path = _dd_as_tuple(cursor_path)
564
+ sub_open = open_path[:len(row_path)] == row_path
565
+ is_cursor = cursor_path == row_path
566
+ tag = _dd_row_lookup(row_tags, value)
567
+
568
+ hovered = draw_state._bounding_hovered
569
+ # Colour the row by its value's embedded tint (e.g. a Lora's .tint), falling
570
+ # back to the per-row override (autocomplete's symbol tints), then the menu
571
+ # tint for plain values.
572
+ row_tint = _dd_row_lookup(row_tints, value)
573
+ tint = _dd_obj_tint(value, row_tint or tint)
574
+ fa_chrevron_right = f"\uf054"
575
+
576
+ kbd_mode = getattr(root_state, "_kbd_mode", True)
577
+ if hovered and not kbd_mode:
578
+ _dd_set_cursor(root_state, row_path, is_branch,
579
+ menu_ds=draw_state.parent_window)
580
+
581
+ active = is_cursor if kbd_mode else hovered
582
+ if active:
583
+ dl = imgui.get_window_draw_list()
584
+ dl.add_rect_filled(draw_state.abs_left, draw_state.abs_top,
585
+ draw_state.abs_left + draw_state.width,
586
+ draw_state.abs_top + draw_state.height,
587
+ pack_color(1, 1, 1, 0.16),
588
+ rounding=getattr(draw_state, 'corner_radius', 6))
589
+
590
+ chevron = f" {fa_chrevron_right}" if is_branch else " " # fa-chevron-right
591
+ if not full_render:
592
+ # Lightweight row: draw the label with raw imgui.text() instead of the
593
+ # full button/draw_text render_func. There's no click report from
594
+ # imgui.text(), so derive one from this row's hover (the caller's
595
+ # bounding-box hover) plus a fresh left mouse-down.
596
+ imgui.text(f"{label}{chevron}")
597
+ clicked = hovered and imgui.is_mouse_clicked(0)
598
+ elif is_branch:
599
+ # +0.9 value on the active row lifts the label over the white@0.16 wash
600
+ # (button's own +1.5 hover boost keys off `hovered`, which is False for
601
+ # the keyboard-cursor row); hue is retained, only brightness moves.
602
+ clicked, _ = button(f"{label}{chevron}", name=f"{label}_ddrow", width=draw_state.content_width - 10,
603
+ height=Toggles.Dropdown.row_height, hovered=hovered, text_color=Tint.dd_text(requested_tint=tint),
604
+ text_saturation=1.349, shadow=False,
605
+ rounding=0, show_button_bg=False, show_bg=False, use_cache=True,
606
+ text_align=text_align, tint=tint)
607
+ else:
608
+ clicked, _ = button(f"{label}{chevron}", name=f"{label}_ddrow", show_button_bg=False,
609
+ width=draw_state.content_width - 10, height=Toggles.Dropdown.row_height, hovered=hovered,
610
+ text_saturation=0.716, z_offset=0, shadow=False, show_bg=False, use_cache=True,
611
+ text_align=text_align, tint=tint)
612
+
613
+ # In keyboard-select mode the arrow keys paint the highlight; hover neither
614
+ # moves the cursor nor paints, until the mouse moves (draw_dropdown clears it).
615
+
616
+ # ONE highlight, framed to this row's box. Mouse mode keys off the live hover;
617
+ # keyboard mode keys off the cursor. Using a separate source per mode (rather
618
+ # than hover OR cursor) avoids briefly painting both the stale-cursor row and
619
+ # the freshly-hovered row, which doubled the wash and looked inconsistent.
620
+
621
+ # Dim kind tag, right-aligned over the row (drawn last so it sits above the
622
+ # highlight). The name is left-aligned by the caller's text_align. A long
623
+ # label could run under a long tag, so the tag gets an opaque backing rect
624
+ # first: the parent window's actual bg fill (bg_color_stack top = nearest
625
+ # show_bg ancestor) with the active-row wash (white @ 0.16, see above)
626
+ # re-composed in, so the mask is invisible on both active and highlighted
627
+ # rows while still cutting off the label.
628
+ if tag:
629
+ _dd_paint_tag(imgui.get_window_draw_list(),
630
+ draw_state.abs_left + draw_state.width - 10,
631
+ draw_state.abs_top, draw_state.height, tag, active)
632
+
633
+ if is_branch:
634
+ # Always call the sub-menu (so off-path ones stay registered but hidden
635
+ # via closed=True, never leaving a stale painted frame); only the on-path
636
+ # branch actually draws. Pinned to the right of this row with window_pos.
637
+ search = str(getattr(root_state, "search", "") or "")
638
+ if any(search in str(key).lower() for key in row_path):
639
+ search = ""
640
+ popup_width, popup_height, _ = _dd_popup_geometry(value, search, 0, 0, False)
641
+ popup_left, popup_top = _dd_submenu_position(
642
+ draw_state.abs_left, draw_state.abs_top, draw_state.width,
643
+ popup_width, popup_height, *imgui.get_io().display_size)
644
+ cursor_x, cursor_y = imgui.get_cursor_screen_pos()
645
+ changed, picked, _sub_ds = draw_dd_menu(value, name=f"{label}_submenu", tint=tint,
646
+ closed=not sub_open, temp=True, use_cache=False,
647
+ window_pos=(popup_left - cursor_x, popup_top - cursor_y),
648
+ width=popup_width, height=popup_height, auto_resize=False,
649
+ show_add_delete=False,
650
+ parent_window=draw_state, disable_scroll=False,
651
+ full_render=full_render, row_tints=row_tints,
652
+ root_state=root_state, path_prefix=row_path,
653
+ return_extras=True)
654
+ # Register the submenu window on the shared root state so the ROOT
655
+ # menu body can sweep it close even when THIS row stops rendering -
656
+ # a search query can filter the row itself out of this level while its
657
+ # submenu is stamped open, and a row that doesn't render can never
658
+ # restamp `closed` (the nested-window leak, one level down).
659
+ if root_state is not None and _sub_ds is not None:
660
+ _reg = getattr(root_state, "_dd_submenu_ds", None)
661
+ if _reg is None:
662
+ _reg = root_state._dd_submenu_ds = {}
663
+ _reg[row_path] = _sub_ds
664
+ if _sub_ds is not None:
665
+ # The closed -> open EDGE: the submenu's row tiles are cached and
666
+ # blit-skip while clean, so a reopened submenu came up as a blank
667
+ # panel until the pointer entered it (a meltygui app's menu bar,
668
+ # 09-14). Cascade-invalidate its rows the frame it opens.
669
+ was_open = getattr(_sub_ds, "_dd_was_open", False)
670
+ if sub_open and not was_open:
671
+ _dd_invalidate_rows(root_state, _sub_ds)
672
+ _sub_ds._dd_was_open = sub_open
673
+ if changed:
674
+ return True, picked
675
+ elif clicked:
676
+ _dd_pick(root_state, row_path)
677
+ return True, value
678
+
679
+ return False, value
680
+
681
+
682
+
683
+ def _dd_fit_label(s, px):
684
+ """`s` ellipsis-trimmed to render within `px` (imgui text metrics), so a
685
+ fixed-width trigger never draws wider than its own button rect."""
686
+ if px <= 0:
687
+ return ""
688
+ if imgui.calc_text_size(s)[0] <= px:
689
+ return s
690
+ while s and imgui.calc_text_size(s + "...")[0] > px:
691
+ s = s[:-1]
692
+ return s + "..."
693
+
694
+
695
+ def _dd_obj_tint(obj, fallback=None):
696
+ """An object's embedded tint (a 3+-tuple `.tint`, e.g. on a Lora), else
697
+ `fallback`. Used to colour each row by its value and the trigger by the
698
+ selected value."""
699
+ t = getattr(obj, "tint", None)
700
+ if isinstance(t, (tuple, list)) and len(t) >= 3:
701
+ return tuple(t)
702
+ return fallback
703
+
704
+
705
+ def _dd_submenu_position(left, top, row_width, width, height, display_w, display_h):
706
+ right = left + row_width
707
+ x = right if right + width <= display_w else left - width
708
+ return max(0, min(x, display_w - width)), max(0, min(top, display_h - height))
709
+
710
+
711
+ def _dd_popup_geometry(collection, search, trigger_top, trigger_height,
712
+ open_upwards=None, min_width=None):
713
+ """Content-sized popup contained in the display, independent of old bounds."""
714
+ from meltygui.model.dropdown_model import _dd_visible_entries
715
+ from meltygui.core.cache.tile_cache import snap_int
716
+
717
+ rows = _dd_visible_entries(collection, (search or "").strip().lower())
718
+ display_w, display_h = imgui.get_io().display_size
719
+ natural_height = (len(rows) + (2 if len(collection) > 4 else 1)) * Toggles.Dropdown.row_height
720
+ above = max(0, trigger_top)
721
+ below = max(0, display_h - trigger_top - trigger_height)
722
+ upwards = (below < min(natural_height, Toggles.Dropdown.max_height) and above > below
723
+ if open_upwards is None else open_upwards)
724
+ height = min(natural_height, Toggles.Dropdown.max_height, above if upwards else below)
725
+ width = min(display_w, max(
726
+ Toggles.Dropdown.min_width if min_width is None else min_width,
727
+ max((imgui.calc_text_size(row[2])[0] + 48 for row in rows), default=0)))
728
+ top = trigger_top - height if upwards else trigger_top + trigger_height
729
+ return snap_int(width), snap_int(height), snap_int(top)
730
+
731
+
732
+ def _dd_menu_fit(menu_ds, min_width=None, max_height=None):
733
+ """The popover size that fits its content — what the wrapper's
734
+ auto_resize computed for a closable window: the measured UNCLIPPED
735
+ group rect (_content_rect), width floored at min_width, height capped
736
+ at max_height and the display (rows scroll past it). None until the
737
+ body has measured."""
738
+ from meltygui.core.cache.tile_cache import snap_int
739
+
740
+ rect = getattr(menu_ds, "_content_rect", None)
741
+ if not rect or rect[0] <= 0 or rect[1] <= 0:
742
+ return None
743
+ display_w, display_h = imgui.get_io().display_size
744
+ fit_w = snap_int(max(min(rect[0], display_w), Toggles.Dropdown.min_width if min_width is None else min_width))
745
+ fit_h = snap_int(max(min(rect[1], display_h, Toggles.Dropdown.max_height if max_height is None else max_height), Toggles.Dropdown.min_height))
746
+ return (fit_w, fit_h)
747
+
748
+
749
+ def _dd_tag_segments(tag):
750
+ """A row tag as [(text, rgba)] segments. A plain str is ONE segment in
751
+ the default dim colour; a sequence mixes str items with (text, color)
752
+ pairs that keep their own colour — a 3-tuple colour gets the default
753
+ alpha, a None colour the default colour. Empty texts drop out."""
754
+
755
+ if not tag:
756
+ return []
757
+ if isinstance(tag, str):
758
+ return [(tag, Toggles.Dropdown.tag_color)]
759
+ out = []
760
+ for item in tag:
761
+ if isinstance(item, str):
762
+ text, color = item, None
763
+ else:
764
+ text, color = item
765
+ color = Toggles.Dropdown.tag_color if color is None else tuple(color)
766
+ if len(color) == 3:
767
+ color = color + (Toggles.Dropdown.tag_color[3],)
768
+ if text:
769
+ out.append((str(text), color))
770
+ return out
771
+
772
+
773
+ def _dd_tag_width(tag):
774
+ """Painted width of a row tag (all segments + gaps), 0 for none."""
775
+
776
+ segments = _dd_tag_segments(tag)
777
+ if not segments:
778
+ return 0.0
779
+ return (sum(imgui.calc_text_size(text)[0] for text, _c in segments)
780
+ + Toggles.Dropdown.tag_gap * (len(segments) - 1))
781
+
782
+
783
+ def _dd_paint_tag(draw_list, right, top, height, tag, active,
784
+ row_tint=None, fade=0.0):
785
+ """The dim tag column, right-aligned to `right` inside a row box
786
+ [top, top + height). Opaque backing rect first so a long label can't
787
+ run under it: the menu's actual painted fill (bg_color_stack top),
788
+ with the row's tint wash and the active-row wash (white @ 0.16)
789
+ re-composed in, so the mask is invisible on plain, tinted and
790
+ highlighted rows alike. `fade` (0..1) pulls every segment's colour
791
+ toward that fill — a faded row's tag fades with its label."""
792
+
793
+ segments = _dd_tag_segments(tag)
794
+ if not segments:
795
+ return
796
+ line_h = imgui.get_text_line_height()
797
+ tag_w = _dd_tag_width(tag)
798
+ tag_x = right - tag_w
799
+ tag_y = top + (height - line_h) * 0.5
800
+ bg = Melty.bg_color_stack[-1][:3] if Melty.bg_color_stack else None
801
+ if bg is not None:
802
+ r, g, b = bg
803
+ if row_tint is not None:
804
+ r = r * (1 - Toggles.Dropdown.row_tint_alpha) + row_tint[0] * Toggles.Dropdown.row_tint_alpha
805
+ g = g * (1 - Toggles.Dropdown.row_tint_alpha) + row_tint[1] * Toggles.Dropdown.row_tint_alpha
806
+ b = b * (1 - Toggles.Dropdown.row_tint_alpha) + row_tint[2] * Toggles.Dropdown.row_tint_alpha
807
+ if active:
808
+ r, g, b = r * 0.84 + 0.16, g * 0.84 + 0.16, b * 0.84 + 0.16
809
+ mask = pack_color(min(max(r, 0.0), 1.0),
810
+ min(max(g, 0.0), 1.0),
811
+ min(max(b, 0.0), 1.0), 1.0)
812
+ draw_list.add_rect_filled(tag_x - 6, top + 1, tag_x + tag_w + 6,
813
+ top + height - 1, mask)
814
+ fade = min(max(float(fade or 0.0), 0.0), 1.0)
815
+ x = tag_x
816
+ for text, color in segments:
817
+ red, green, blue, alpha = color
818
+ if fade and bg is not None:
819
+ red = red * (1 - fade) + bg[0] * fade
820
+ green = green * (1 - fade) + bg[1] * fade
821
+ blue = blue * (1 - fade) + bg[2] * fade
822
+ draw_list.add_text(x, tag_y,
823
+ pack_color(red, green, blue, alpha),
824
+ text)
825
+ x += imgui.calc_text_size(text)[0] + Toggles.Dropdown.tag_gap
826
+
827
+
828
+ def _dd_row_width(draw_state):
829
+ """A menu row's width: the popover WINDOW's own width less the scrollbar
830
+ reserve. NOT content_width — the wrapper derives that from the PARENT's
831
+ available width (the trigger's 300 px slot) and pins it at min_width,
832
+ so a popover grown to fit long labels painted its tags and hit-tested
833
+ its rows at 300 px while the window was 600+ wide (the tag masked the
834
+ label's tail). The max keeps the old figure where it was the larger."""
835
+ window_width = draw_state.width or 0
836
+ return max(draw_state.content_width or 0,
837
+ window_width - (SCROLL_BAR_WIDTH_DEFAULT + SCROLLBAR_MARGIN))
838
+
839
+
840
+ def _dd_leaf_row(key, value, label, draw_state, root_state, path_prefix,
841
+ cursor_path, tint=None, row_tags=None, row_tints=None,
842
+ row_suffixes=None, row_actions=None, left_pad=10,
843
+ text_toward_bg=0.0, row_code=None, code_label_w=None,
844
+ row_width=None):
845
+ """Render ONE leaf menu row inline with raw imgui — NO per-row render_func.
846
+ Leaves are the bulk of a big menu, so skipping the dd_menu_row wrapper (its
847
+ own draw_state / cache / BVH / hover machinery, tens of µs each) is the whole
848
+ point: it's what made the 967-icon list crawl while hovering/scrolling. The
849
+ parent draw_dd_menu tile already re-renders every frame it's bounding-hovered
850
+ (core_render hover invalidation), so this row's hover highlight / click stay
851
+ live without a tile of its own. Branch rows still go through dd_menu_row — they
852
+ own a nested submenu and a real draw_state. Returns the picked value when
853
+ clicked, else UNSET_VALUE.
854
+
855
+ `draw_state` is the MENU window's draw_state (the level), not a per-row one."""
856
+ from meltygui.core.conversion.cache_tree import UNSET_VALUE
857
+ from meltygui.core.layout.dropdown_core import _dd_pick
858
+ from meltygui.core.layout.dropdown_core import _dd_set_cursor
859
+ from meltygui.editor.source_ui import _RowSpan
860
+ from meltygui.editor.source_ui import _row_code_hosts
861
+ from meltygui.view.text_view import draw_text
862
+ from meltygui.model.dropdown_model import _dd_as_tuple
863
+ from meltygui.model.dropdown_model import _dd_row_lookup
864
+ from meltygui.core.windowing.glfw_utils import request_render
865
+
866
+ row_path = tuple(path_prefix) + (key,)
867
+ is_cursor = _dd_as_tuple(cursor_path) == row_path
868
+ kbd_mode = getattr(root_state, "_kbd_mode", True)
869
+ row_fade = float(text_toward_bg or 0.0)
870
+
871
+ pos = imgui.get_cursor_screen_pos()
872
+ x, y = pos[0], pos[1]
873
+ w = row_width if row_width else _dd_row_width(draw_state)
874
+ h = Toggles.Dropdown.row_height
875
+ mp = imgui.get_mouse_pos()
876
+ hovered = (x <= mp[0] < x + w) and (y <= mp[1] < y + h)
877
+ if hovered:
878
+ # Clamp hover to the menu window's visible band: rows laid out above the
879
+ # window bounds (scrolled/clipped away) are invisible but this raw imgui
880
+ # math would still hit them - a click on editor text under the popup
881
+ # could pick an unseen row.
882
+ _wt = draw_state._abs_top()
883
+ if not (_wt <= mp[1] < _wt + (draw_state.height or 0)):
884
+ hovered = False
885
+ if hovered and not kbd_mode:
886
+ _dd_set_cursor(root_state, row_path, False, menu_ds=draw_state)
887
+
888
+ active = is_cursor if kbd_mode else hovered
889
+ dl = imgui.get_window_draw_list()
890
+ line_h = imgui.get_text_line_height()
891
+ # Per-row symbol tint (the autocomplete popup's background colors): drawn
892
+ # as a colored BACKGROUND wash with near-white text over it - the editor's
893
+ # wash styling - rather than colored text. Under the active highlight so
894
+ # keyboard/hover selection still reads on tinted rows.
895
+ row_tint = _dd_row_lookup(row_tints, value)
896
+ _ROW_TINT_A = Toggles.Dropdown.row_tint_alpha
897
+ _ROW_TINT_V_CAP = 0.55 # max RGB component - near-white text must stay legible
898
+ _ROW_TINT_S_BOOST = 1.25 # saturation bump on capped tints — keeps hue vivid
899
+ if row_tint is not None:
900
+ r, g, b = row_tint[:3]
901
+ v = max(r, g, b)
902
+ if v > _ROW_TINT_V_CAP:
903
+ k = _ROW_TINT_V_CAP / v
904
+ r, g, b = r * k, g * k, b * k
905
+ # Boost saturation by pulling the lower channels up from the max
906
+ # (raw channel arithmetic - hue is the channel ORDER, matters).
907
+ m = max(r, g, b)
908
+ r = max(0.0, m - (m - r) * _ROW_TINT_S_BOOST)
909
+ g = max(0.0, m - (m - g) * _ROW_TINT_S_BOOST)
910
+ b = max(0.0, m - (m - b) * _ROW_TINT_S_BOOST)
911
+ dl.add_rect_filled(x, y + 1, x + w, y + h - 1,
912
+ pack_color(r, g, b, _ROW_TINT_A),
913
+ rounding=getattr(draw_state, 'corner_radius', 6))
914
+ if active:
915
+ dl.add_rect_filled(x, y, x + w, y + h,
916
+ pack_color(1, 1, 1, 0.16),
917
+ rounding=getattr(draw_state, 'corner_radius', 6))
918
+
919
+ # Raw imgui.text_colored() for the label, coloured by the value's embedded
920
+ # tint (e.g. a Lora's .tint) falling back to the menu tint - same source as
921
+ # dd_menu_row. Vertically centred in the fixed-height row: set_cursor pins the
922
+ # next row exactly h below (matches the full menu's item_spacing.y=0), so
923
+ # leaves and dd_menu_row branches line up.
924
+ if row_tint is not None:
925
+ # White nudged toward the wash color: legible on the tinted bar while
926
+ # still reading as that symbol's hue.
927
+ color = tuple(min(1.0, c * 0.25 + 0.75) for c in row_tint[:3])
928
+ else:
929
+ color = _dd_obj_tint(value, tint)
930
+ color = Tint.dd_text(requested_tint=color)
931
+ # Quiet-row dimming: pull the label toward the menu bg so tinted
932
+ # rows (the info tab's active-source yellow) stand out - menu-wide
933
+ # (text_toward_bg). Skipped for washed rows above - their label is
934
+ # already contrast-managed.
935
+ if row_fade and Melty.bg_color_stack:
936
+ _bg = Melty.bg_color_stack[-1]
937
+ _k = min(max(row_fade, 0.0), 1.0)
938
+ color = tuple(c * (1 - _k) + _b * _k
939
+ for c, _b in zip(color[:3], _bg[:3]))
940
+ imgui.set_cursor_screen_pos((x + left_pad, y + (h - line_h) * 0.5))
941
+
942
+ if active:
943
+ # Active-row label: lerp toward white so it clears the white@0.16 wash
944
+ # (the wash lightens the row fill while the label is its plain text
945
+ # value - light-on-light, 2.8:1 in a dark-tinted editor). Keeping 40%
946
+ # of the original chroma leaves per-item tints recognizable.
947
+ color = tuple(min(1.0, c * 0.4 + 0.6) for c in color[:3])
948
+ color = *(color[:3]), 1.0
949
+
950
+ tag = _dd_row_lookup(row_tags, value)
951
+ code_row = _dd_row_lookup(row_code, value)
952
+ if code_row is not None:
953
+ # GlobalSearch-mirrored code row (the quick-jump popup): the row
954
+ # label as a dim prefix, then the ACTUAL code line rendered through the
955
+ # real editor - draw_text with the file's live cst-dict parse and a
956
+ # jump_to line-offset shim, so token colors and definition washes are
957
+ # pixel-identical to the jump window; the file line shown in the
958
+ # editor's own gutter (line_numbers=[...]) and the editor's text
959
+ # right-aligns like every other row (width reserved below). Same
960
+ # embed recipe as draw_global_search's code_row block, including
961
+ # use_cache=False (the layer-band masking note there).
962
+ _cp, _cl, _ccode = code_row
963
+ _ty = y + (h - line_h) * 0.5
964
+ # rstrip: dedup keys may carry invisible trailing whitespace - never a
965
+ # visible counter. Elipsize past the
966
+ # label cap so a deep scope name can't eat the code column.
967
+ _lbl = str(label).rstrip()
968
+ if imgui.calc_text_size(_lbl)[0] > Toggles.Dropdown.code_label_max_width:
969
+ while _lbl and imgui.calc_text_size(_lbl + "…")[0] > Toggles.Dropdown.code_label_max_width:
970
+ _lbl = _lbl[:-1]
971
+ _lbl += "…"
972
+ dl.add_text(x + left_pad, _ty,
973
+ pack_color(color[0], color[1], color[2], 0.9),
974
+ _lbl)
975
+ # Shared column (widest label in the menu, capped, precomputed by
976
+ # draw_dd_menu) so every row's editor starts at the same x - with the
977
+ # static 5-digit gutter inside draw_text, code aligns line to line.
978
+ _lw = (code_label_w if code_label_w is not None
979
+ else min(imgui.calc_text_size(_lbl)[0], Toggles.Dropdown.code_label_max_width))
980
+ _cx = x + left_pad + _lw + 14.0
981
+ _tw_r = (_dd_tag_width(tag) + 22.0) if tag else 10.0
982
+ _cw = max(60.0, x + w - _tw_r - _cx)
983
+ # Offscreen rows (scrolled past the menu's visible band): skip the
984
+ # draw_text embed - the one-line editor body takes real wrapper
985
+ # time, and a tall menu lays out every row each frame - and reserve
986
+ # the space with a dummy so layout/scroll stay constant.
987
+ _wt = draw_state._abs_top()
988
+ _wb = _wt + (draw_state.height or 0)
989
+ if y + h <= _wt or y >= _wb:
990
+ imgui.set_cursor_screen_pos((_cx, y))
991
+ imgui.dummy(_cw, h)
992
+ else:
993
+ _cdict, _chost = _row_code_hosts(_cp)
994
+ imgui.set_cursor_screen_pos((_cx, y))
995
+ try:
996
+ # use_cache=True: the menu re-renders every bounding-hovered
997
+ # frame, and an uncached editor body per row makes big pickers
998
+ # crawl - cached editor tiles blit-skip when clean. Same
999
+ # masking-cliff caveat as the GlobalSearch code rows.
1000
+ _res = draw_text(_ccode, name=f"dd_code_{key}", show_header=False,
1001
+ show_bg=False, shadow=True, single_line=True,
1002
+ width=_cw, height=h, use_cache=False, bg_offset=-2,
1003
+ jump_to=_RowSpan(_cl - 1, _cp), is_search_box=True,
1004
+ is_tree=False, z_offset=2,
1005
+ show_jump_bar=False, line_numbers=[_cl],
1006
+ tint=row_tint, selectable=False,
1007
+ roster_live_hold=False, # single-only preview
1008
+ return_extras=True)
1009
+ if _chost is not None:
1010
+ # Repaint when the background parse lands (washes pop in) but
1011
+ # the ROW's own cached tile, not the menu window (the window
1012
+ # invalidation leaves clean row tiles blit-skipped).
1013
+ _row_ds = _res[2] if len(_res) > 2 else None
1014
+ _chost.notify_on_change(_row_ds if _row_ds is not None
1015
+ else draw_state)
1016
+ except Exception:
1017
+ traceback.print_exc()
1018
+ # The embedded editor's caret sub consumes clicks over the code
1019
+ # area - reclaim them with a higher-priority sub on exactly that
1020
+ # rect (draw_global_search's code rows use the same technique) so
1021
+ # the click still picks the row. Label/tag/action areas keep the
1022
+ # raw imgui click path below.
1023
+ if draw_state.on_action("left_mouse_down",
1024
+ view_id=f"dd_code_act_{key}",
1025
+ rect=(_cx, y, _cx + _cw, y + h),
1026
+ priority_delta=4) is not None:
1027
+ _dd_pick(root_state, row_path)
1028
+ imgui.set_cursor_screen_pos((x, y + h))
1029
+ return value
1030
+ imgui.set_cursor_screen_pos((x, y + h))
1031
+ else:
1032
+ imgui.text_colored(str(label), *color)
1033
+
1034
+ # Dim '(param, param2)' suffix right after the callable's name (autocomplete
1035
+ # rows): same hue as the label at reduced alpha, so it stays subtle on
1036
+ # plain, tinted and active rows alike. imgui text (not raw dl.add_text) so
1037
+ # the row's measured width includes it and auto-resizes with the popup.
1038
+ sfx = _dd_row_lookup(row_suffixes, value)
1039
+ if sfx:
1040
+ imgui.same_line(spacing=0)
1041
+ imgui.text_colored(sfx, color[0], color[1], color[2], 0.45)
1042
+ # Claim the tag column's width as row content too, so the menu's
1043
+ # auto-resize fits label AND tag side by side instead of the tag
1044
+ # masking the label's tail (a wide tag - commit date - hid most of a
1045
+ # long commit subject label).
1046
+ if tag:
1047
+ imgui.same_line(spacing=0)
1048
+ imgui.dummy(_dd_tag_width(tag) + 16.0, 1)
1049
+
1050
+ imgui.set_cursor_screen_pos((x, y + h))
1051
+
1052
+ # Dim kind tag, right-aligned (autocomplete's func/class/… label; the
1053
+ # Compare With rows' date) - _dd_paint_tag masks the label under it and
1054
+ # fades the hovered row.
1055
+ if tag:
1056
+ _dd_paint_tag(dl, x + w - 10, y, h, tag, active, row_tint=row_tint,
1057
+ fade=row_fade)
1058
+
1059
+ # Per-row ACTION (`row_actions`: dict value→callable, or single callable for
1060
+ # every row): a right-aligned trash icon whose click runs the action and
1061
+ # CONSUMES the click - no pick, popover stays open, so several rows can
1062
+ # be acted on in one visit (the info tab's clear-at-action).
1063
+ _act = None
1064
+ if row_actions is not None:
1065
+ _act = (_dd_row_lookup(row_actions, value)
1066
+ if isinstance(row_actions, dict) else row_actions)
1067
+ if _act is not None:
1068
+ _ax = x + w - 24
1069
+ _over_act = hovered and mp[0] >= _ax - 4
1070
+ dl.add_text(_ax, y + (h - line_h) * 0.5,
1071
+ pack_color(0.85, 0.32, 0.28,
1072
+ 0.95 if _over_act else 0.4),
1073
+ "")
1074
+ if _over_act and imgui.is_mouse_clicked(0):
1075
+ _act(value)
1076
+ request_render()
1077
+ return UNSET_VALUE
1078
+
1079
+ if hovered and imgui.is_mouse_clicked(0):
1080
+ _dd_pick(root_state, row_path)
1081
+ return value
1082
+ return UNSET_VALUE