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,848 @@
1
+ """Header view functions and supporting definitions."""
2
+ from meltygui.hdr_color import pack_color
3
+ from meltygui.core.melty import Melty
4
+ from meltygui.core.melty import add_to_collection
5
+ from meltygui.core.rendering.render_funcs import RenderFuncs
6
+ from meltygui.state.core_enums import ProfileMode
7
+ from meltygui.state.new_core_model import TileMode
8
+ from meltygui.core.runtime.toggles import Tint
9
+ from meltygui.core.runtime.toggles import Toggles
10
+ from meltygui_imgui.core import _DrawList
11
+ from types import NoneType
12
+ import colorsys
13
+ import meltygui_imgui as imgui
14
+ import types
15
+
16
+
17
+ def render_search(search_ds, draw_state, unique=None, width=None, regrab_focus=True):
18
+ """Render the find UI for the searchable view whose state lives on
19
+ `search_ds`: the search input, match count, prev/next nav, and close.
20
+
21
+ Shared by draw_header (inline, when the view has a header) and draw_search
22
+ (a floating window, when it doesn't). All state — search_text, match count,
23
+ current index — lives on `search_ds`, the owning view's draw_state, so both
24
+ presentations drive the same search.
25
+
26
+ `regrab_focus=False` limits the focus claim to first open (plus the Ctrl+F
27
+ one-shot): an always-visible box (the input tab's filter) must not pull
28
+ focus back whenever nothing holds text focus, or other fields on the same
29
+ tab become untypeable once focus clears.
30
+ """
31
+ from meltygui.core.windowing.glfw_utils import request_render
32
+ import meltygui.core.windowing.window_api as glfw
33
+
34
+
35
+ from meltygui.view.text_view import draw_text
36
+ # Grab focus on first open, and re-grab whenever nothing holds text focus.
37
+ # Window focus management (move-to-front / window activation) clears
38
+ # text_focused_ds when a window comes forward that doesn't contain the
39
+ # focused field. The floating search box lives in its own window, so
40
+ # without re-grabbing it would drop focus after the first keypress. The
41
+ # re-grab runs before draw_text's key handling, so no keystroke is lost,
42
+ # and it won't steal focus from a deliberate click into the editor (which
43
+ # leaves text_focused_ds non-None).
44
+ # `_search_focus_pending` is the one-shot set when Ctrl+F opened the search:
45
+ # claim focus this frame regardless of who holds text focus (the underlying
46
+ # searchable view can reclaim meltygui text focus before this box renders, so
47
+ # "text_focused_ds is None" alone misses the just-opened case). Consume it so
48
+ # later frames fall back to the gentle re-grab and don't fight a deliberate
49
+ # click into the editor.
50
+ # The re-grab (reclaim focus when nothing holds text focus) is gated on this
51
+ # search still being the active one (focused_ds is the owner). A deliberate
52
+ # click away runs clear_focus, which clears focused_ds, so the box releases
53
+ # focus and stays open-but-unfocused. A spurious clear during typing leaves
54
+ # focused_ds intact, so the box reclaims focus and no keystroke is lost.
55
+ # First-open and the Ctrl+F one-shot grab regardless.
56
+ focus_search = ((not search_ds._search_was_active)
57
+ or (regrab_focus and Melty.text_focused_ds is None
58
+ and Melty.focused_ds is search_ds)
59
+ or search_ds._search_focus_pending)
60
+ # One-shot open/Ctrl+F frame (NOT the spurious-clear regrab): select the
61
+ # whole term so typing replaces it and a single delete clears it.
62
+ focus_fresh = ((not search_ds._search_was_active)
63
+ or search_ds._search_focus_pending)
64
+ search_ds._search_focus_pending = False
65
+ search_ds._search_was_active = True
66
+ search_icon = ""
67
+ imgui.align_text_to_frame_padding()
68
+ imgui.text(search_icon)
69
+ imgui.same_line()
70
+
71
+ if width is None:
72
+ width = draw_state.content_width
73
+ _box = draw_text(search_ds.search_text, searchable=False,
74
+ is_search_box=True,
75
+ shadow=False, max_height=40,
76
+ name=search_icon + str(unique), with_header=None,
77
+ with_header_end=None, max_width=width - 50,
78
+ with_footer=None, header_same_line=True, tint=search_ds.tint,
79
+ show_name=False, show_header=False, single_line=True,
80
+ request_focus=focus_search, select_all_on_focus=focus_fresh,
81
+ return_extras=True)
82
+ search_change, new_search = _box[0], _box[1]
83
+ _box_ds = _box[2] if len(_box) > 2 else None
84
+ # While the find box holds text focus, mark this search as the active one so
85
+ # Enter/arrow nav routes here — including after clicking back into the box,
86
+ # where the click restores text focus but not focused_ds.
87
+ if _box_ds is not None and Melty.text_focused_ds is _box_ds:
88
+ Melty.focused_ds = search_ds
89
+ if search_change:
90
+ search_ds.search_text = new_search
91
+ # Re-render the owner's whole subtree so every child view recomputes its
92
+ # matches against the new term and the combined count stays in sync.
93
+ Melty.cache.invalidate_up(search_ds._tile_id, force=True, max_depth=12)
94
+ request_render()
95
+ # initial use
96
+
97
+
98
+ # Match count + prev/next navigation. The count and current index are
99
+ # populated by the searchable view's body (e.g. the text editor); the
100
+ # arrows step the active match and ask the body to scroll it into view.
101
+
102
+ imgui.same_line()
103
+ from meltygui.view.control_view import button
104
+ fa_x_icon = ""
105
+
106
+ imgui.set_cursor_screen_pos((draw_state.abs_left + width-25, imgui.get_cursor_screen_pos()[1]))
107
+ # imgui.set_cursor_screen_pos((imgui.get_cursor_screen_pos()[0], imgui.get_cursor_screen_pos()[1] + 2))
108
+ if button(fa_x_icon, name=f"{unique}##fa_x_icon", show_bg=False,
109
+ use_cache=True, height=23, shadow=True, z_offset=3, max_height=40,
110
+ tile_mode=TileMode.MAX, color=(9, 1, 1, 0))[0]:
111
+ search_ds.search_active = False
112
+ search_ds._search_was_active = False
113
+ # Keep search_text so reopening the find bar restores the last query.
114
+ Melty.text_focused_ds = None
115
+
116
+ total = search_ds.text_search_count
117
+ if total > 0:
118
+ imgui.align_text_to_frame_padding()
119
+ imgui.text_colored(f"{search_ds.text_search_current + 1}/{total}",
120
+ 0.66, 0.74, 0.82, 1.0)
121
+ imgui.same_line(spacing=2)
122
+ nav = 0
123
+ if imgui.small_button(f"##search_prev{unique}"):
124
+ nav = -1
125
+ imgui.same_line(spacing=2)
126
+ if imgui.small_button(f"##search_next{unique}"):
127
+ nav = 1
128
+ # Enter / Down = find next, Shift+Enter / Up = find prev, Ctrl+Enter =
129
+ # "click" the selected result — but only while the FIND BOX (not the
130
+ # underlying editor) holds text focus, so Enter still inserts newlines
131
+ # when you click into the editor. We gate on the box holding text focus
132
+ # directly rather than on `focused_ds is search_ds`: clicking back into
133
+ # the box runs clear_focus, which nulls focused_ds (the searched view
134
+ # isn't under the click to be protected), so keying off focused_ds
135
+ # silently dropped Enter-nav after a mouse refocus. text_focused_ds is
136
+ # set straight by the box's own click handler, so it survives that.
137
+ # The find box is single-line, so Up/Down don't move its cursor and are
138
+ # free for stepping matches. Drained from the GLFW-callback key queue
139
+ # (not imgui.is_key_pressed) so it isn't dropped on slow frames.
140
+ if _box_ds is not None and Melty.text_focused_ds is _box_ds:
141
+ if any(k == glfw.KEY_DOWN for k, _ in Melty.frame_key_events):
142
+ nav = 1
143
+ elif any(k == glfw.KEY_UP for k, _ in Melty.frame_key_events):
144
+ nav = -1
145
+ # Enter steps to the next match, Shift+Enter to the previous, and
146
+ # HOLDING Enter rapid-fires — imgui's synthesized auto-repeat
147
+ # (io.key_repeat_delay/rate) is read here because GLFW's REPEAT events
148
+ # don't reach the key queue on every platform (Wayland); keep the loop
149
+ # rendering while Enter is held so that cadence is sampled. Ctrl+Enter
150
+ # "clicks" the selected result and stays single-shot (from the queue).
151
+ _enter = [m for k, m in Melty.frame_key_events
152
+ if k in (glfw.KEY_ENTER, glfw.KEY_KP_ENTER)]
153
+ _enter_repeat = (imgui.is_key_pressed(glfw.KEY_ENTER, repeat=True)
154
+ or imgui.is_key_pressed(glfw.KEY_KP_ENTER, repeat=True))
155
+ if imgui.is_key_down(glfw.KEY_ENTER) or imgui.is_key_down(glfw.KEY_KP_ENTER):
156
+ request_render()
157
+ if _enter or _enter_repeat:
158
+ if _enter and (_enter[-1] & glfw.MOD_CONTROL):
159
+ # "Click" the selected result: hit-test the BVH at the center
160
+ # of the selected match's rect and send a mouse-down to the
161
+ # front-most view there (the actual clickable, e.g. a managed
162
+ # window's name button) — exactly what a real click resolves
163
+ # to. The view's own click handling does the rest (toggle a
164
+ # window, focus an input, …). Queued + the tile invalidated so
165
+ # it re-renders and reads the click next frame (the find UI
166
+ # renders too late to inject for this frame).
167
+ from meltygui.core.automation.search_core import search_activate_target
168
+ from meltygui.core.input.input_handler import InputEvent
169
+ _target = search_activate_target(Melty.search_current_node)
170
+ if _target is not None and _target.width and _target.height:
171
+ _cx = _target.abs_left + _target.width / 2.0
172
+ _cy = _target.abs_top + _target.height / 2.0
173
+ _hits = [h for h in Melty.bvh_query(_cx, _cy)
174
+ if h._tile_id is not None and not h.just_shadow]
175
+ _top = _hits[0] if _hits else _target
176
+ Melty.search_click_pending = (
177
+ _top._tile_id,
178
+ InputEvent("left_mouse", "down", tile_id=_top._tile_id, x=_cx, y=_cy))
179
+ # Re-render the owner's subtree next frame (same as nav) so
180
+ # the clicked view actually re-runs and reads the injection.
181
+ Melty.cache.invalidate_up(search_ds._tile_id, force=True, max_depth=12)
182
+ request_render()
183
+ elif not imgui.get_io().key_ctrl:
184
+ nav = -1 if imgui.get_io().key_shift else 1
185
+
186
+ if nav != 0:
187
+ # total is the combined count across all views; stepping wraps over
188
+ # the whole result set. Flag a scroll and invalidate the owner's
189
+ # subtree so every child view recomputes and the one holding the new
190
+ # global-current match scrolls to it.
191
+ search_ds.text_search_current = (search_ds.text_search_current + nav) % total
192
+ search_ds._search_nav_pending = True
193
+ Melty.cache.invalidate_up(search_ds._tile_id, force=True, max_depth=12)
194
+ request_render()
195
+ elif search_ds.search_text:
196
+ imgui.align_text_to_frame_padding()
197
+ imgui.text_colored("No results", 0.74, 0.5, 0.5, 1.0)
198
+ # Enter with the find box focused force-recomputes the result set. "No
199
+ # results" can be stale — the searched views may have been rebuilt since
200
+ # the count was last taken (e.g. a fresh load from disk) — so re-run the
201
+ # cross-view walk on demand rather than leaving it stuck at zero. Flagging
202
+ # _search_nav_pending makes the owner's pre-body walk re-count next frame
203
+ # (same key source + box-focus gate as the nav block above).
204
+ if _box_ds is not None and Melty.text_focused_ds is _box_ds:
205
+ if any(k in (glfw.KEY_ENTER, glfw.KEY_KP_ENTER)
206
+ for k, _ in Melty.frame_key_events):
207
+ search_ds._search_nav_pending = True
208
+ Melty.cache.invalidate_up(search_ds._tile_id, force=True, max_depth=12)
209
+ request_render()
210
+ else:
211
+ imgui.align_text_to_frame_padding()
212
+ imgui.text_colored("", 0.74, 0.5, 0.5, 1.0)
213
+
214
+
215
+ def flat_button(label, draw_state, view_id, width=None, height=None,
216
+ color=(0.533, 0.068, 0.5), tint_value=0.16, text_value=1.023,
217
+ factor=1.0, saturation=1.2, text_saturation=0.8, alpha=1.0,
218
+ corner_radius=6.0, text_pad=15, hover_boost=0.05,
219
+ hover_text_boost=2.2, max_bg_brightness=0.25,
220
+ event="left_mouse_clicked", text_offset_x=None,
221
+ style_manager=None, layout=True, draw_list=None,
222
+ shadow=True, shadow_offset=2.0, text_color=None, pos=None,
223
+ hovered=None, style=None, **kwargs):
224
+ """Draw-list button — the fast-dock interaction model instead of a
225
+ @render_func widget (~0.7ms of wrapper per call, measured): a rounded
226
+ rect + centered label straight to the draw list, hover from the live
227
+ mouse position (the owning tile repaints every frame while
228
+ bounding-hovered, so the highlight tracks), and the click claimed
229
+ through the OWNING view's draw_state.on_action rect — the same routing
230
+ the tab bar's dnd clicks already used, so blit-cache event delivery
231
+ holds. Advances the flow like an inline item of the same size (dummy).
232
+ Styling mirrors `button`'s make_color_rgb + brightness-clamp pipeline so
233
+ converted call sites keep their look. alpha=0 draws no bg (label-only
234
+ buttons, e.g. inactive tabs). Returns True on click.
235
+
236
+ pos=(x, y) / hovered=: draw-only callers outside any view (the OS-window
237
+ controls in titlebar.py, on the overlay list with their own hit logic)
238
+ place the button and drive its hover state themselves instead of the
239
+ cursor position and the owning draw_state."""
240
+ from meltygui.core.styling.style import Style
241
+ from meltygui.core.cache.tile_cache import add_shadow
242
+ from meltygui.core.layout.header_runtime import _TEXT_COLOR_MEMO
243
+ from meltygui.model.color_model import _brightness_clamp
244
+ import meltygui.core.input.mouse_cursor as mouse_cursor
245
+
246
+ if style_manager is None:
247
+ style_manager = Melty.style_manager
248
+ scale = Melty.ui_scale # Melty.px inlined: ~170 calls a frame
249
+ text = str(label).split("##")[0]
250
+ ts = imgui.calc_text_size(text)
251
+ w = width if width is not None else ts.x + text_pad * scale
252
+ h = height if height is not None else ts.y + 8.0 * scale
253
+ x, y = pos if pos is not None else imgui.get_cursor_screen_pos()
254
+ mx, my = imgui.get_mouse_pos()
255
+ if hovered is None:
256
+ hovered = (draw_state is not None
257
+ and draw_state._bounding_hovered
258
+ and not Melty.on_drag
259
+ and x <= mx < x + w and y <= my < y + h)
260
+ # draw_list: paint into a caller-provided list instead of the window's
261
+ # (e.g. the overlay list a DragDrop ghost rides) — pairs with layout=False.
262
+ dl = draw_list if draw_list is not None else imgui.get_window_draw_list()
263
+ if alpha > 0.0 and color is not None:
264
+ # Shadow under any button that draws a bg — a standalone depth mark
265
+ # (no draw_state for the compositor to shadow; the default offset +2
266
+ # mirrors the legacy active-button z_offset lift). Label-only buttons
267
+ # (alpha=0, e.g. inactive tabs) cast nothing, matching the old
268
+ # per-call-site marks. layout=False draw-only ghosts skip it too —
269
+ # they ride an overlay list outside the mark's snapshotted clip.
270
+ # shadow_offset = the lift (depth delta); a smaller one sits the
271
+ # button lower, so e.g. inactive tabs stay under the active tab.
272
+ rounding = corner_radius * scale
273
+ if shadow and layout:
274
+ add_shadow((x, y, w, h), offset=shadow_offset,
275
+ corner_radius=rounding)
276
+ if Toggles.dynamic_styles:
277
+ # Legacy callers supply a hue; its strength becomes a residual.
278
+ # Explicit Style values can supply signed shifts or absolutes.
279
+ bg_style = style
280
+ if bg_style is None:
281
+ strength = tint_value + (hover_boost if hovered else 0.0)
282
+ bg_style = Style(tuple(c * strength for c in color[:3]) + (alpha,))
283
+ Melty.add_background(bg_style, rect=(x, y, w, h),
284
+ corner_radius=rounding, draw_list=dl)
285
+ else:
286
+ clamp = _brightness_clamp
287
+ bg = style_manager.make_color_rgb(
288
+ color[0], color[1], color[2],
289
+ value=tint_value + (hover_boost if hovered else 0.0),
290
+ factor=factor, saturation_scale=saturation, alpha=1.0)
291
+ bg = clamp(bg[0], bg[1], bg[2], 0.0, max_bg_brightness)
292
+ dl.add_rect_filled(x, y, x + w, y + h,
293
+ pack_color(bg[0], bg[1], bg[2], alpha),
294
+ rounding=rounding)
295
+ # text_color: use this exact rgb for the label instead of the theme-mix
296
+ # pipeline below — that pipeline only lets text_value/text_saturation
297
+ # touch `factor` worth of the final color (the rest is the raw `color`),
298
+ # so callers needing FULL-range text control (the editor tabs' hsv
299
+ # knobs) pre-compute the color and pass it here. Hover still brightens.
300
+ if text_color is not None:
301
+ tc_key = (text_color[0], text_color[1], text_color[2], hovered)
302
+ tc = _TEXT_COLOR_MEMO.get(tc_key)
303
+ if tc is None:
304
+ th, tsat, tv = colorsys.rgb_to_hsv(*text_color[:3])
305
+ if hovered:
306
+ tv = min(1.0, tv + 0.25)
307
+ tc = colorsys.hsv_to_rgb(th, tsat, tv)
308
+ if len(_TEXT_COLOR_MEMO) > 2048:
309
+ _TEXT_COLOR_MEMO.clear()
310
+ _TEXT_COLOR_MEMO[tc_key] = tc
311
+ else:
312
+ tc = style_manager.make_color_rgb(
313
+ color[0], color[1], color[2],
314
+ value=text_value + (hover_text_boost if hovered else 0.0),
315
+ factor=factor, saturation_scale=text_saturation, alpha=1.0)
316
+ # text_offset_x: left-align the label at a fixed inset instead of
317
+ # centering — for buttons whose left edge hosts another element (the
318
+ # editor tabs' tint swatch) that centered text would overlap.
319
+ # Optical-centering nudges (same as the fast dock / `button`): glyphs sit
320
+ # low-left of their geometric cell, so shift right and up a hair.
321
+ tx = x + text_offset_x if text_offset_x is not None else x + (w - ts.x) * 0.5
322
+ dl.add_text(tx + 2.0 * scale, y + (h - ts.y) * 0.5 - scale,
323
+ pack_color(tc[0], tc[1], tc[2], 1.0), text)
324
+ # layout=False: draw-only — no dummy (nothing submitted to the window
325
+ # group, so an out-of-flow draw like a DragDrop ghost can't stretch the
326
+ # view's measured content) and no click subscription.
327
+ if not layout:
328
+ return False
329
+ imgui.dummy(w, h)
330
+ if draw_state is None:
331
+ return False
332
+ # `event` picks the trigger: the default full click, or "left_mouse_down"
333
+ # for press-reactive controls (tab switches) that should feel immediate.
334
+ # cursor=ARROW: a button always shows the plain pointer, whatever shape
335
+ # the view it sits in carries (inline buttons in draw_text sit inside
336
+ # the text body's I-beam rect). priority_delta=4 outranks the body's
337
+ # own cursor registrations (draw_text's is at 3), and being an
338
+ # on_action from the owner's body it is replayed on its cache hits.
339
+ fired = draw_state.on_action(event, view_id=view_id,
340
+ rect=(x, y, x + w, y + h),
341
+ priority_delta=4,
342
+ cursor=mouse_cursor.ARROW) is not None
343
+ # Effect ledger: a fired flat button is an observable effect with no
344
+ # undo record — the Orchestrator cues replays off it (view_id names the
345
+ # button; the rect gives the cue press-fraction geometry, so button
346
+ # clicks generalize like leaf-editor presses).
347
+ if fired and Melty.effect_hook is not None:
348
+ try:
349
+ Melty.effect_hook("button", str(view_id).split("##")[0], draw_state,
350
+ rect=(x, y, w, h))
351
+ except Exception:
352
+ pass
353
+ return fired
354
+
355
+
356
+ def draw_header_arrow(expanded, color=None, alpha=0.071):
357
+ """The header's transparent tree control, also usable by flat views.
358
+ Dimmed by Toggles.Melty.arrow_brightness (every arrow, everywhere)."""
359
+ from meltygui.core.runtime.toggles import Toggles
360
+ dim = float(Toggles.Melty.arrow_brightness)
361
+ alpha = alpha * dim
362
+ if color is None:
363
+ depth = max(0.0, Melty.bg_depth)
364
+ color = Melty.style_manager.make_color_style_value(input={
365
+ "value": (7.788 + (depth - 30.0) * 0.05 * 0.332) * dim,
366
+ "saturation": 1.559 + (depth - 1.773) * -0.004,
367
+ "alpha": alpha, "max_value": 1.601})
368
+ else:
369
+ color = tuple(c * dim for c in color[:3]) + tuple(color[3:])
370
+ imgui.push_style_color(imgui.COLOR_TEXT, *color[:3], 1.0)
371
+ imgui.push_style_color(imgui.COLOR_BUTTON, 0.0, 0.0, 0.0, 0.0)
372
+ imgui.push_style_color(imgui.COLOR_BUTTON_HOVERED, 0.0, 0.0, 0.0, 0.0)
373
+ imgui.push_style_var(imgui.STYLE_ALPHA, alpha)
374
+ try:
375
+ imgui.set_item_allow_overlap()
376
+ return imgui.arrow_button("##tree", imgui.DIRECTION_DOWN if expanded else imgui.DIRECTION_RIGHT)
377
+ finally:
378
+ imgui.pop_style_var()
379
+ imgui.pop_style_color(3)
380
+
381
+
382
+ def draw_header(input_value=None, name="", key=None, meltygui=None, parent_show_add_delete=False, width=7, suffix="",
383
+ collection=None, icon=None, display_name=None, meta=None, unique=None, is_tree=True,
384
+ show_name=True, name_func=None, show_type=False, show_unique=False, name_color=None,
385
+ on_search=False, trigger_collapse=False, trigger_expand=False, header_same_line=False,
386
+ draw_state=None, show_tint=False, opacity=1.23, show_add_delete=True, new_item_type=types.NoneType,
387
+ show_add_types=None, on_drag=False, on_action=None, style_manager=None, font=None,
388
+ **kwargs):
389
+ # Constants
390
+ from meltygui.core.conversion.bubbling import _BubblingDict
391
+ from meltygui.core.windowing.glfw_utils import request_render
392
+ from meltygui.utils.render_utils import pop_style_var
393
+ from meltygui.utils.render_utils import push_style_var
394
+ from meltygui.view.search_view import draw_search_highlight
395
+ from meltygui.core.layout.cursor_core import same_line
396
+ from meltygui.core.layout.header_core import _jump_to_view_source
397
+ from meltygui.model.collection_model import annotation_item_type
398
+
399
+ _font_pushed = False
400
+
401
+
402
+ if font is not None and Melty.font_mgr is not None:
403
+ _font_handle = Melty.font_mgr.get(font)
404
+ if _font_handle is not None:
405
+ imgui.push_font(_font_handle)
406
+ _font_pushed = True
407
+
408
+
409
+ # Depth drives name brightness
410
+ depth_scale = 0.05
411
+ depth_offset = -30.0
412
+ # Depth drives text saturation falloff
413
+ sat_depth_factor = -0.004
414
+ sat_depth_offset = -1.773
415
+
416
+ draw_list = imgui.get_window_draw_list()
417
+ depth = max(0.0, Melty.bg_depth)
418
+ depth_intensity = float(depth + depth_offset) * depth_scale
419
+
420
+ sat_shift = float(depth + sat_depth_offset) * sat_depth_factor
421
+ # Name text (value is the base offset, updated by depth below)
422
+ name_style = {
423
+ 'value': 1.188, 'saturation': 0.778,
424
+ 'alpha': 0.174, 'max_value': 3.921,
425
+ 'depth_factor': 0.729
426
+ }
427
+ name_rounding = 2.696
428
+ max_name_chars = 40
429
+ min_name_text_width = 62
430
+
431
+ # Type / unique label colors
432
+ type_label_tint = (3.672, 1.944, 2.861, 1.0)
433
+ unique_label_tint = (-1.535, 0.0, 0.9, 1.0)
434
+
435
+ # Depth-driven color computati
436
+ # on
437
+ name_style['value'] = depth_intensity * name_style['depth_factor'] + name_style['value']
438
+ name_style['saturation'] = name_style['saturation'] + sat_shift
439
+
440
+ if name_color is not None:
441
+ name_color = style_manager.make_color_style_rgb(*name_color, input=name_style, factor=0.1)
442
+ else:
443
+ name_color = style_manager.make_color_style_value(input=name_style, value=0.5)
444
+ arrow_style = {
445
+ 'value': 7.788, 'saturation': 1.559,
446
+ 'alpha': 0.071, 'max_value': 1.601,
447
+ 'depth_factor': 0.332
448
+ }
449
+
450
+
451
+ # imgui.same_line(False) # does not work
452
+ # imgui.set_cursor_position(0,0) # does not work
453
+
454
+ # imgui.set_cursor_pos((0,0))
455
+ # def set_cursor(tuple_in):
456
+ # pass
457
+
458
+ # set_cursor((0,0)) # fine
459
+ # set_cursor(0,0) # This one works
460
+ # set_cursor(*(0,0)) # does not work
461
+
462
+
463
+ arrow_style['value'] = depth_intensity * arrow_style['depth_factor'] + arrow_style['value']
464
+ arrow_style['saturation'] = arrow_style['saturation'] + sat_shift
465
+ arrow_color = style_manager.make_color_style_value(input=arrow_style)
466
+
467
+ # ── Tree arrow ─────────────────────────────────────────────
468
+ imgui.dummy(5, 0)
469
+ start_x, start_y = imgui.get_cursor_screen_pos()
470
+
471
+ on_change = False
472
+ return_val = on_action
473
+ push_style_var(imgui.STYLE_ALPHA, opacity)
474
+ if display_name is not None:
475
+ name = display_name
476
+ imgui.align_text_to_frame_padding()
477
+
478
+ if is_tree:
479
+ imgui.set_cursor_screen_pos(imgui.get_cursor_screen_pos())
480
+ imgui.dummy(0, 0)
481
+ imgui.same_line(spacing=0)
482
+
483
+ if draw_header_arrow(draw_state.expanded, arrow_color, arrow_style['alpha']):
484
+ draw_state.expanded = not draw_state.expanded
485
+ # Effect ledger: expand/collapse is draw_state UI state — no
486
+ # undo record — but it IS the gate the orchestration machinery
487
+ # keys on (the field-behind-a-collapsed-parent story). The
488
+ # direction rides in the kind so replay verification catches a
489
+ # wrong toggle (an "expand" replayed onto an already-expanded
490
+ # view collapses it — the cue then fails honestly).
491
+ if Melty.effect_hook is not None:
492
+ try:
493
+ Melty.effect_hook(
494
+ "expand" if draw_state.expanded else "collapse",
495
+ str(getattr(draw_state, "name", "?")).split("##")[0],
496
+ draw_state)
497
+ except Exception:
498
+ pass
499
+ draw_state.content_height = 0
500
+ draw_state.invalid_content_height = True
501
+ request_render()
502
+ same_line()
503
+ else:
504
+ imgui.same_line(spacing=0)
505
+
506
+ # ── Type / unique labels ───────────────────────────────────
507
+ if show_type:
508
+ imgui.text_colored(f"({input_value.__class__.__name__})", *type_label_tint)
509
+ same_line()
510
+ if show_unique:
511
+ imgui.text_colored(f"({str(Melty.get_tile_id())})", *unique_label_tint)
512
+ same_line()
513
+ if show_name and name != "":
514
+ same_line(spacing=0)
515
+ imgui.set_item_allow_overlap()
516
+
517
+ # ── Tint widget ────────────────────────────────────────────
518
+ # A dict can carry its tint in __overrides__ (parsed from a `# [tint=(...)]`
519
+ # comment); edit that store directly so the change round-trips to source.
520
+ # The override comment is itself the opt-in, so this isn't gated on
521
+ # show_tint (which is only set for top-level windows, not nested classes).
522
+ # (Legacy per-storage tint chain removed — the anywhere swatch below IS
523
+ # the tint widget: one loop for every source, set_anywhere on write.)
524
+ _overrides = input_value.get("__overrides__") if isinstance(input_value, dict) else None
525
+
526
+ # ── Anywhere tint swatch (outlined) ── one attribute, both directions:
527
+ # `draw_state.locate_tint` READS the framework-resolved tint
528
+ # (draw_state._kwargs, in-flight cache included) and ASSIGNING it writes
529
+ # back to whichever source DRIVES it — code, comment, decoration,
530
+ # instance attr. (The property pair lives in anywhere.py; this is its
531
+ # first caller.) Outlined so the swatch reads apart from the labels.
532
+ from meltygui.core.rendering.parameter_core import get_source_for
533
+ _aw_attr = "tint"
534
+ _aw_tint = draw_state.locate_tint
535
+ if Toggles.dynamic_styles and draw_state.locate_style is not None:
536
+ _aw_attr = "style"
537
+ _aw_tint = draw_state.locate_style
538
+
539
+ # The caption re-resolves this often while it still reads the draw_state
540
+ # fallback: the view's code hosts load in the background, so the first
541
+ # popover open (which creates them) resolves before the source that
542
+ # really drives the tint has parsed — a meltygui app's `@glfw_window(tint=)`
543
+ # showed as "draw_state" until a write forced a fresh collection (09-12).
544
+ # [tint=(0.85, 0.55, 0.25)]
545
+ source_retry_frames = 30
546
+
547
+ def _aw_source_info(_ds=draw_state, _attr=_aw_attr):
548
+ # Popover caption: the LAST-KNOWN driving source. Reads the cache
549
+ # set_anywhere maintains; resolves (one collection) on first popover
550
+ # open, then caches on the ds — re-resolving only while the answer is
551
+ # the draw_state fallback (see source_retry_frames), never per frame.
552
+ _last = getattr(_ds, "_sa_last_source", None)
553
+ _src = _last.get(_attr) if _last else None
554
+ _frame = Melty.frame_count
555
+ _stale = (_src == "draw_state"
556
+ and _frame - getattr(_ds, "_sa_source_frame", -source_retry_frames) >= source_retry_frames)
557
+ if _src is None or _stale:
558
+ _src = get_source_for(_attr, _ds)
559
+ _ds._sa_source_frame = _frame
560
+ if _src is not None:
561
+ if _last is None:
562
+ _last = {}
563
+ _ds._sa_last_source = _last
564
+ _last[_attr] = _src
565
+ return f" {_src}" if _src else None # FA location-arrow
566
+
567
+ if _aw_tint is not None and (show_tint or (
568
+ isinstance(_overrides, dict) and _overrides.get("tint") is not None)):
569
+ draw_state._has_popup = True
570
+ # Named on the header's own `unique` — NEVER Melty.get_tile_id(),
571
+ # which reflects the current tile pass and shifts during edits
572
+ # (popover open, forced invalidations), re-keying the widget mid-drag
573
+ # so its fresh draw_state re-measures and the row wraps. The outline
574
+ # is drawn by draw_tuple around the chip itself (outline=True) — the
575
+ # widget's draw_state box is far wider than the swatch.
576
+ imgui.same_line(spacing=0)
577
+ # draw_tuple_fast, not the draw_tuple render_func: every window
578
+ # header paid a full wrapper call per frame for this 17 px chip
579
+ # (~0.19 ms each, use_cache=False). The chip claims its own 17×17
580
+ # footprint here since the fast path draws without layout.
581
+ from meltygui.view.collection_view import draw_tuple_fast
582
+ _aw_x, _aw_y = imgui.get_cursor_screen_pos()
583
+ _aw_ch, _aw_val = draw_tuple_fast(
584
+ _aw_tint, draw_state, view_id="aw_tint", x=_aw_x, y=_aw_y,
585
+ size=17, outline=True, info=_aw_source_info,
586
+ setter=lambda value, _ds=draw_state, _attr=_aw_attr: setattr(_ds, "locate_" + _attr, value),
587
+ view_owner=draw_state)
588
+ imgui.dummy(17, 17)
589
+ if _aw_ch:
590
+ setattr(draw_state, "locate_" + _aw_attr, _aw_val)
591
+ same_line()
592
+
593
+ # ── Add button ─────────────────────────────────────────────
594
+ if show_add_delete and (isinstance(input_value, (list, dict, _BubblingDict)) or hasattr(input_value, "__dict__")):
595
+ def _instantiate_and_add(item_type):
596
+ if item_type is NoneType:
597
+ item_type = annotation_item_type(kwargs.get("annotation")) or NoneType
598
+ try:
599
+ new_item = item_type()
600
+ except Exception as e:
601
+ print(f"Could not instantiate {item_type} for add: {e}")
602
+ new_item = None
603
+ add_to_collection(input_value, new_item)
604
+
605
+
606
+ if show_add_delete:
607
+ if flat_button(f"\uf067##add{unique}", draw_state,
608
+ view_id=f"hdr_add{unique}"):
609
+ _instantiate_and_add(new_item_type)
610
+ on_change = True
611
+ return_val = input_value
612
+ same_line()
613
+
614
+ # ── Typed add button ───────────────────────────────────
615
+ # A second + button whose item type comes from show_add_types — a
616
+ # {display_name: type} dict (a bare list/tuple keys itself by
617
+ # __name__). With several entries a small chevron dropdown picks the
618
+ # type: the pick persists BY DISPLAY NAME on draw_state.misc (a plain
619
+ # str survives save/load and hotswap; the type is re-resolved from
620
+ # show_add_types each frame) and the + relabels to "+ <name>". A
621
+ # single entry needs no choice — no chevron, the + is pre-bound to
622
+ # it. Until a type is picked the + falls back to the same hint-based
623
+ # default as the plain add button.
624
+ if show_add_types:
625
+ if not isinstance(show_add_types, dict):
626
+ show_add_types = {t.__name__: t for t in show_add_types}
627
+ if len(show_add_types) == 1:
628
+ sel_name = next(iter(show_add_types))
629
+ else:
630
+ sel_name = draw_state.misc.get("add_type_name")
631
+ sel_type = show_add_types.get(sel_name)
632
+ add_label = f"\uf067 {sel_name}" if sel_type is not None else "\uf067"
633
+ if flat_button(f"{add_label}##add_typed{unique}", draw_state,
634
+ view_id=f"hdr_add_typed{unique}"):
635
+ _instantiate_and_add(sel_type if sel_type is not None else new_item_type)
636
+ on_change = True
637
+ return_val = input_value
638
+ if len(show_add_types) > 1:
639
+ same_line(spacing=0)
640
+ _dd_pos = imgui.get_cursor_screen_pos()
641
+ type_changed, picked = RenderFuncs.draw_dropdown(
642
+ "\uf078", collection=show_add_types, z_offset=-1,
643
+ name=f"add_type{unique}", width=20, show_header=False, show_bg=False, show_button_bg=False,
644
+ show_name=False, shadow=False)
645
+ if type_changed and any(v is picked for v in show_add_types.values()):
646
+ draw_state.misc["add_type_name"] = next(
647
+ k for k, v in show_add_types.items() if v is picked)
648
+ draw_state.invalidate()
649
+ request_render()
650
+ # The dropdown's (hidden) popover window leaves the imgui cursor on
651
+ # a new line; same_line() would chain off that, so restore the
652
+ # cursor to just right of the chevron for the name that follows.
653
+ # Width mirrors the trigger's own sizing (passed width, with the
654
+ # glyph + compact pad 6 as the floor) so the name never starts
655
+ # inside the button.
656
+ _chev_w = max(24, imgui.calc_text_size("\uf078")[0] + 6)
657
+ imgui.set_cursor_screen_pos((_dd_pos[0] + _chev_w + 3, _dd_pos[1]))
658
+ else:
659
+ same_line()
660
+
661
+
662
+ # ── Name label / edit ──────────────────────────────────────
663
+ has_visible_name = show_name and name not in ("", None, "None")
664
+
665
+ if has_visible_name:
666
+ clipped_name = name.split("##")[0][:max_name_chars] + " "
667
+ if header_same_line:
668
+ text_width = imgui.calc_text_size(clipped_name)[0] - 4
669
+ else:
670
+ text_width = imgui.calc_text_size(clipped_name)[0]
671
+
672
+ if icon is not None:
673
+ imgui.align_text_to_frame_padding()
674
+ # The icon glyph renders 2px below the name text baseline.
675
+ _ix, _iy = imgui.get_cursor_screen_pos()
676
+ imgui.set_cursor_screen_pos((_ix, _iy - 2))
677
+ imgui.text_colored(icon, *Tint.icon_tint())
678
+ imgui.same_line()
679
+ _nx, _ny = imgui.get_cursor_screen_pos()
680
+ imgui.set_cursor_screen_pos((_nx, _ny + 2))
681
+
682
+ push_style_var(imgui.STYLE_FRAME_ROUNDING, name_rounding)
683
+
684
+ if not draw_state._name_edit:
685
+
686
+ draw_list: _DrawList = imgui.get_window_draw_list()
687
+ cursor_pos = imgui.get_cursor_screen_pos()
688
+ # Search-match highlight behind the key name (drawn before the text
689
+ # so glyphs stay readable). Strong fill + outline for the active
690
+ # (global-current) match; a faint fill for the rest. The flags are
691
+ # set by draw_collection when this header's key matches the query.
692
+ # Lifted to a HIGHER depth channel (restore after, the
693
+ # DrawState.draw_rect idiom): the halo spills past this row's rect,
694
+ # and at the content channel a sibling row's background composites
695
+ # over the spill — cropped edges and bake-order flicker.
696
+ if kwargs.get("search_match", False):
697
+ hx0, hy0 = cursor_pos[0], cursor_pos[1]
698
+ hx1 = cursor_pos[0] + text_width
699
+ hy1 = cursor_pos[1] + imgui.get_text_line_height()
700
+ if Melty.channels_split:
701
+ draw_list.channels_set_current(
702
+ min(Melty.get_channel() + 2, Melty.max_depth - 1))
703
+ draw_search_highlight(draw_list, hx0, hy0, hx1, hy1,
704
+ current=kwargs.get("search_current", False))
705
+ if Melty.channels_split:
706
+ draw_list.channels_set_current(Melty.get_channel())
707
+ packed_name_color = pack_color(*name_color[:3], 1.0)
708
+ draw_list.add_text(cursor_pos[0], cursor_pos[1], packed_name_color, clipped_name)
709
+ imgui.dummy(text_width, imgui.get_frame_height())
710
+ pop_style_var(1)
711
+ else:
712
+ name_width = min(text_width, min_name_text_width)
713
+ imgui.set_next_item_width(name_width)
714
+ edit_flags = imgui.INPUT_TEXT_ENTER_RETURNS_TRUE | imgui.INPUT_TEXT_AUTO_SELECT_ALL
715
+ changed, new_name = imgui.input_text(f"##edit{name}_{unique}", name, flags=edit_flags)
716
+ pop_style_var(1)
717
+
718
+ if changed or imgui.is_key_pressed(imgui.KEY_ESCAPE) or not imgui.is_item_active():
719
+ draw_state._name_edit = False
720
+
721
+ same_line(spacing=0)
722
+
723
+ end_x = imgui.get_cursor_screen_pos()[0]
724
+
725
+ # ── Ctrl+B on the header → the view function's source ──────
726
+ # Same landing as a global-search Code hit (_jump_to_symbol_def): the
727
+ # editor opens on the `def` of whatever function renders this view.
728
+ # Hover-scoped to the header's own rect, so the editor's Ctrl+B (usage
729
+ # jump, hover-routed to the text body) is untouched.
730
+ header_rect = (start_x, start_y, end_x, start_y + imgui.get_frame_height())
731
+ if draw_state.on_action("ctrl_b_down", view_id=f"hdr_jump{unique}",
732
+ rect=header_rect) is not None:
733
+ # Fire-and-forget flash on the header itself so the jump reads as
734
+ # "from here" while the editor comes to front (same flash as the
735
+ # editor's own jump landing / merge Apply).
736
+ Melty.emphasize(f"hdr_jump {draw_state.name}",
737
+ (header_rect[0] - 3, header_rect[1] - 2,
738
+ header_rect[2] + 3, header_rect[3] + 2))
739
+ _jump_to_view_source(draw_state)
740
+
741
+ # ── Profiler ───────────────────────────────────────────────
742
+ is_profiling = Toggles.profile_mode == ProfileMode.ON
743
+ if is_profiling:
744
+ from meltygui.view.diagnostic_view import render_profiler_time
745
+ render_profiler_time(
746
+ input_value=draw_state.render_time, brief=True,
747
+ style_manager=style_manager,
748
+ )
749
+ same_line(spacing=3)
750
+
751
+ pop_style_var(1)
752
+
753
+ if _font_pushed:
754
+ imgui.pop_font()
755
+ # Record the natural (pre-pad) header width so core_render can fold it into
756
+ # the parent window's running max for the next frame.
757
+ draw_state.header_natural_width = end_x - start_x
758
+ if kwargs.get("align_header", True) and show_name:
759
+ # Pad to the widest header in this window (tracked per-window), with the
760
+ # preferred width as a floor. Falls back to the floor when no window.
761
+ parent_window = draw_state.parent_window
762
+ pad_target = Toggles.Collection.preferred_header_width
763
+ if parent_window is not None:
764
+ pad_target = max(pad_target, parent_window.max_header_width)
765
+ if end_x - start_x < pad_target:
766
+ imgui.dummy(pad_target - (end_x - start_x), 1)
767
+ imgui.same_line(8)
768
+
769
+ return on_change, return_val
770
+
771
+
772
+ def draw_footer(input_value=None, name="", key=None, meltygui=None, parent_show_add_delete=False, width=0, suffix="",
773
+ collection=None, display_name=None, meta=None, unique=None, is_tree=True,
774
+ show_name=True, name_func=None, show_type=False, show_unique=False,
775
+ on_search=False, trigger_collapse=False, trigger_expand=False,
776
+ draw_state=None, show_tint=False, opacity=1.0, show_add_delete=True,
777
+ on_drag=False, on_action=None, style_manager=None,
778
+ **kwargs):
779
+
780
+ # for key, pending in draw_state._all_pending.items():
781
+ # if pending is not None:
782
+ # if pending.state == PendingState.ERROR:
783
+ # from meltygui.core.rendering.render_dispatch import draw_pending
784
+ # draw_pending(pending, name=f"{key}", tint=(1, 0, 0))
785
+
786
+ imgui.dummy(1,1)
787
+
788
+
789
+ def draw_header_end(input_value=None, name="", show_close=True, key=None, meltygui=None, parent_show_add_delete=False,
790
+ collection=None, draw_state=None, closable=False, style_manager=None,
791
+ unique=None, show_tint=False, **kwargs):
792
+ from meltygui.core.layout.cursor_core import same_line
793
+
794
+ close_icon = ""
795
+ # Frame-redraw indicator (left of the close button): alternates these
796
+ # two glyphs every frame, so a header that repaints continuously flickers.
797
+ redraw_icon_0 = ""
798
+ redraw_icon_1 = ""
799
+ # [tint=(1.0, 1.0, 1.0)]
800
+ redraw_alpha = 0.1
801
+ redraw_pad_x = 3
802
+
803
+ if closable:
804
+ if show_tint:
805
+ # Paint-only (no dummy / same_line): the glyph must not claim
806
+ # layout in the right-aligned end-header group, or it would
807
+ # push the close button left. It hangs off the button's left
808
+ # edge, centered on the row height flat_button gives the
809
+ # button (text + px(8)).
810
+ button_height = imgui.calc_text_size(close_icon)[1] + Melty.px(8.0)
811
+ redraw_icon = [redraw_icon_0, redraw_icon_1][Melty.frame_count % 2]
812
+ # One slot as wide as the wider glyph, each glyph centered in it —
813
+ # anchoring by the glyph's own width made the two alternate ~2px apart.
814
+ slot_width = max(imgui.calc_text_size(redraw_icon_0)[0],
815
+ imgui.calc_text_size(redraw_icon_1)[0])
816
+ icon_width, icon_height = imgui.calc_text_size(redraw_icon)
817
+ button_x, row_top = imgui.get_cursor_screen_pos()
818
+ icon_x = button_x - redraw_pad_x - slot_width + (slot_width - icon_width) / 2
819
+ icon_y = row_top + (button_height - icon_height) / 2
820
+ redraw_color = pack_color(1, 1, 1, redraw_alpha)
821
+ imgui.get_window_draw_list().add_text(icon_x, icon_y, redraw_color, redraw_icon)
822
+ if show_close:
823
+ if flat_button(f"{close_icon}##{unique}", draw_state,
824
+ view_id=f"hdr_close{unique}",
825
+ color=(9, 1, 1)):
826
+ _was_closed = draw_state.closed
827
+ from meltygui.core.windowing.window_visibility import native_user_window_closed
828
+ native_user_window_closed(draw_state, not draw_state.closed)
829
+ from meltygui.state.core_undo import NavUndo
830
+ from meltygui.core.windowing.window_visibility import window_edit_is_local
831
+ if window_edit_is_local(draw_state, 'closed'):
832
+ NavUndo.record_window(draw_state, _was_closed, draw_state.closed)
833
+ Melty.cache.invalidate_up_by_obj(Melty.registered_windows)
834
+
835
+ # draw_state._parent.invalidate_up()
836
+ if draw_state.parent_window is not None:
837
+ draw_state.parent_window.invalidate_up()
838
+
839
+ draw_state.dlt_count = 0
840
+ # if draw_state.parent_window is not None:
841
+ # Melty.cache.invalidate_up(draw_state.parent_window._tile_id, max_depth=10)
842
+ else:
843
+ if parent_show_add_delete and collection is not None and key is not None:
844
+ if flat_button(f"\uf1f8##del{unique}", draw_state,
845
+ view_id=f"hdr_del{unique}",
846
+ color=(0.12, 0.002037035, 0.002037035), alpha=0.4):
847
+ Melty.to_delete(key, collection)
848
+ same_line(spacing=0.0)