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,258 @@
1
+ """Tab view functions and supporting definitions."""
2
+ from meltygui.core.melty import Melty
3
+ from meltygui.core.core_render import render_func
4
+ from meltygui.core.rendering.core_decoration import Core
5
+ from meltygui.state.new_core_model import TabState
6
+ from meltygui.view.header_view import draw_header
7
+ import meltygui_imgui as imgui
8
+
9
+
10
+ @render_func(is_tree=False, show_bg=True, shadow=False, use_cache=False, header_same_line=True,
11
+ disable_scroll=True,
12
+ indent_size=0, show_add_delete=False,
13
+ show_name=False, selectable=False, parent_show_add_delete=False,
14
+ with_header=draw_header)
15
+ def draw_tab_bar(input_value: list, tab_height=30, names=None, tint_value=0.235, tint_saturation=0.372, unique=None,
16
+ collection=None, as_toggles=False, tints=None, icons=None, excluded=None, width=None,
17
+ dnd_collection_ds=None, dnd_keys=None,
18
+ draw_state=None):
19
+ """Tab bar with multi-select via shift-click. input_value is the list of selected items, collection is all available tabs.
20
+ Tabs wrap onto a new row when the cumulative width would exceed draw_state.content_width.
21
+
22
+ tints: optional list of (r, g, b) tint colors, one per tab in `collection`. Entries that are
23
+ None (or beyond the list) fall back to the neutral grey. (Defaults to None rather than [] to
24
+ avoid the mutable-default-arg pitfall; behaves identically to an empty list.)
25
+
26
+ dnd_collection_ds + dnd_keys: opt tabs into the framework DragDrop. Each
27
+ tab button registers as a whole-rect drag item of `dnd_collection_ds` (the
28
+ draw_state whose input_value is the collection being reordered — the
29
+ Reorder/Insert lands on ITS return via Melty.dnd_requests). dnd_keys is a
30
+ list parallel to `collection`: (key, collection_index) per tab, or None
31
+ for tabs that aren't draggable (e.g. a synthetic General tab). The owner
32
+ must stamp _dnd_drop_target/_dnd_horizontal on dnd_collection_ds so slot
33
+ lines render vertically between the tabs."""
34
+ from meltygui.view.control_view import button
35
+ from meltygui.view.header_view import flat_button
36
+ from meltygui.core.layout.cursor_core import same_line
37
+ import meltygui.core.input.drag_drop_core as _drag_drop
38
+
39
+ if collection is None:
40
+ return False, input_value
41
+
42
+ if excluded is None:
43
+ excluded = set()
44
+ # Content-left edge, captured before the dummy/same_line/-10 shift below.
45
+ # The wrap limit is measured from here so it lines up with content_width.
46
+ origin_x = imgui.get_cursor_screen_pos()[0]
47
+
48
+ imgui.dummy(0, 0)
49
+ imgui.same_line()
50
+
51
+ # [tint=(0.894, 0.568, 0.204, 1.0), show_tint=True]
52
+ io = imgui.get_io()
53
+ changed = False
54
+ selected = list(input_value)
55
+ if selected is None:
56
+ selected = []
57
+ if names is None and hasattr(input_value, 'keys') and hasattr(input_value, 'values'):
58
+ input_value = list(input_value.values())
59
+ names = list(input_value.keys())
60
+
61
+ imgui.set_cursor_screen_pos((imgui.get_cursor_screen_pos()[0] - 10, imgui.get_cursor_screen_pos()[1]))
62
+ # First-row start x after the -10 shift; wrapped rows realign to this
63
+ # (new_line() alone resets to the window content x, which is ~10px right).
64
+ row_start_x = imgui.get_cursor_screen_pos()[0]
65
+
66
+ # Mirror button()'s sizing: width = calc_text_size(label_text).x + text_pad (15).
67
+ # Scaled like button() scales its text_pad, so the wrap measurement below
68
+ # matches the width the buttons actually take.
69
+ # [tint=(0.124, 0.65, 0.087, 1.0), show_tint=True]
70
+ button_padding = Melty.px(15)
71
+ # tab_height arrives as an authored-at-1.0 constant (callers pass 30/40),
72
+ # so it scales here - once, up front, so the button, the click test and
73
+ # the drag placeholder below all agree on one height. The drag path
74
+ # deliberately overrides this with the MEASURED pickup height, which is
75
+ # already in real pixels.
76
+ tab_height = Melty.px(tab_height)
77
+ content_width = draw_state.content_width if draw_state is not None else 0
78
+ x_limit = origin_x + content_width if content_width > 0 else None
79
+
80
+ # An explicitly passed width is a real constraint content_width knows
81
+ # nothing about (the context menu passes width=content_width-282) — clamp
82
+ # to it. draw_state.width and abs_clip_rect are NOT constraints here: with
83
+ # wrap=True the wrapper writes the measured content extent back into
84
+ # draw_state.width (and the clip rect derives from it), so clamping to
85
+ # them ratchets the bar narrower on every reflow until each tab fits on
86
+ # its own row.
87
+ if width and draw_state is not None:
88
+ view_right = draw_state._abs_left() + width - 3
89
+ x_limit = view_right if x_limit is None else min(x_limit, view_right)
90
+
91
+ def _tab_text(t):
92
+ return t.name if hasattr(t, 'name') else str(t)
93
+
94
+ for i, tab in enumerate(collection):
95
+ raw = _tab_text(tab)
96
+ # label_text = raw.replace("_", " ")
97
+
98
+ if i in excluded:
99
+ continue
100
+ label = f"{raw}"
101
+ if names is not None and i < len(names):
102
+ label = f"{names[i]}"
103
+ # Icons parallels `collection` like tints; prefix before the## so the
104
+ # imgui id (and click identity) stays keyed on the bare tab name.
105
+ if icons is not None and i < len(icons) and icons[i]:
106
+ label = f"{icons[i]} {label}"
107
+ active = tab in selected
108
+
109
+ tab_color = (0.5, 0.5, 0.5)
110
+ tinted = tints is not None and i < len(tints) and tints[i] is not None
111
+ if tinted:
112
+ tab_color = tints[i]
113
+
114
+ new_value = 0.15 if not tinted else 0.1
115
+
116
+ # make_color_rgb mixes `color` toward the theme color by `factor`; factor=1.0 (button's
117
+ # default) discards `color` entirely. Drop factor for tinted tabs so the tint shows, and
118
+ # give inactive tinted tabs a faint fill (the default alpha=0.0 draws no rect at all).
119
+ tab_factor = 0.30 if tinted else 1.2
120
+
121
+ tab_width = imgui.calc_text_size(label.split("##")[0]).x + button_padding
122
+
123
+ # Wrap before drawing: the previous iteration's same_line() left the
124
+ # cursor at this tab's real start (with item spacing included), so
125
+ # comparing live cursor + width against the content right edge needs
126
+ # no estimated spacing or fudge factor.
127
+ if i > 0 and x_limit is not None and imgui.get_cursor_screen_pos()[0] + tab_width > x_limit:
128
+ imgui.new_line()
129
+ imgui.set_cursor_screen_pos((row_start_x, imgui.get_cursor_screen_pos()[1]))
130
+
131
+ # Framework drag-and-drop: this button acts as a whole-rect drag
132
+ # item of dnd_collection_ds (key= is what DragDrop._begin picks up).
133
+ # While IT is the dragged child it renders with the floating-window
134
+ # kwargs (detached, fixed to pickup size, glued to the cursor) and a
135
+ # plain dummy holds its slot open in the flow.
136
+ dnd = None
137
+ dragged = False
138
+ dnd_extra = {}
139
+ if dnd_collection_ds is not None and dnd_keys is not None and i < len(dnd_keys):
140
+ dnd = dnd_keys[i]
141
+ _tx, _ty = imgui.get_cursor_screen_pos()
142
+ btn_height = tab_height
143
+ if dnd is not None:
144
+ dnd_extra = {"key": dnd[0], "dnd_handle": True, "return_extras": True}
145
+ # Same (collection, key) can be carried by two views - this button
146
+ # and the tab's stacked content view (draw_collection_as_tabs
147
+ # multi-select). Only the view actually picked up detaches, so
148
+ # require the dragged item to BE this button's draw state.
149
+ dragged = (_drag_drop.DragDrop.is_dragged_child(dnd_collection_ds, dnd[0])
150
+ and _drag_drop.DragDrop.item_ds is dnd_collection_ds._children.get(dnd[1]))
151
+ if dragged:
152
+ dnd_extra.update(_drag_drop.DragDrop.dragged_item_kwargs())
153
+ # dragged_item_kwargs pins width/height to the pickup size -
154
+ # height would collide with the explicit height= below, so
155
+ # route it through btn_height (the pinned button size).
156
+ btn_height = dnd_extra.pop("height", btn_height)
157
+
158
+ if dnd is not None:
159
+ # dnd: tabs - legacy @render_func buttons: DragDrop needs a
160
+ # per-tab draw_state to register as the drag child (pickup,
161
+ # floating window, slot placeholder). Only the context menu's
162
+ # reorderable tab bar takes this branch.
163
+ if active:
164
+ selected_value = 0.23
165
+ btn_res = button(label, z_offset=2, name=f"tab_{i}_{unique}",
166
+ height=btn_height, tint_value=new_value + selected_value - 0.03,
167
+ color=tab_color, factor=tab_factor, draw=True, **dnd_extra)
168
+ else:
169
+ saturation = 1.0 if tinted else 0.3
170
+ btn_res = button(label, indent_size=0, height=btn_height, draw=True, z_offset=0.0,
171
+ alpha=0.0 if tinted else 0.0, tint_value=new_value if not tinted else 0.1,
172
+ saturation=saturation,
173
+ name=f"tab_{i}_{unique}_deactivated", color=tab_color, factor=tab_factor,
174
+ text_value=1.0 if not tinted else 0.9,
175
+ shadow=False, **dnd_extra)
176
+ # A draggable tab must not change the selection on mouse-DOWN (a
177
+ # drag pickup would eat a multi-select). Override the button's
178
+ # down-click and select on CLICKED instead: the input handler only
179
+ # passes it for a release within MOVE_MAX_DISTANCE of the press,
180
+ # so a press that becomes a drag never selects.
181
+ clicked = False
182
+ if not dragged and draw_state is not None:
183
+ clicked = draw_state.on_action(
184
+ "left_mouse_clicked", view_id=f"tab_click_{i}",
185
+ rect=(_tx, _ty, _tx + tab_width, _ty + tab_height),
186
+ priority_delta=2) is not None
187
+ btn_ds = btn_res[2] if len(btn_res) == 3 else None
188
+ if btn_ds is not None:
189
+ # Fulfill the drop-collection contract on the OWNER's ds (see
190
+ # DragDrop._is_drop_collection): children keyed by collection
191
+ # index, backlinked for register_item/_begin.
192
+ btn_ds._collection_draw_state = dnd_collection_ds
193
+ if dnd_collection_ds._children is None:
194
+ dnd_collection_ds._children = {}
195
+ dnd_collection_ds._children[dnd[1]] = btn_ds
196
+ if dragged:
197
+ # The dragged button deferred to a floating window and drew
198
+ # nothing inline - hold its slot open at the current flow
199
+ # position so the bar doesn't reflow mid-drag.
200
+ imgui.dummy(tab_width, tab_height)
201
+ clicked = False
202
+ else:
203
+ # Plain tabs: draw-list rendering (flat_button - the fast-dock
204
+ # model). The per-tab @render_func buttons cost ~0.7ms each in
205
+ # wrapper machinery alone; on the editor's 8-tab strip that was
206
+ # the largest single slice of the hovered frame. Visual parity
207
+ # with the old button params: active tabs get the filled rect,
208
+ # inactive draw label-only (alpha=0), hover brightens the text
209
+ # like the button's hovered text_value boost. Framework
210
+ # shadows/z-offset on the active tab are gone (no draw_state) -
211
+ # the fast-dock tradeoff.
212
+ if active:
213
+ # flat_button applies its own shadow when it draws a bg (the
214
+ # inactive alpha=0 tabs stay flat).
215
+ clicked = flat_button(
216
+ label, draw_state, view_id=f"tab_{i}",
217
+ width=tab_width, height=btn_height,
218
+ color=tab_color, factor=tab_factor,
219
+ tint_value=new_value + 0.23 - 0.03)
220
+ else:
221
+ clicked = flat_button(
222
+ label, draw_state, view_id=f"tab_{i}",
223
+ width=tab_width, height=btn_height,
224
+ color=tab_color, factor=tab_factor, alpha=0.0,
225
+ tint_value=new_value if not tinted else 0.1,
226
+ saturation=1.0 if tinted else 0.3,
227
+ text_value=1.0 if not tinted else 0.9)
228
+
229
+ if clicked:
230
+ changed = True
231
+ if io.key_shift or as_toggles:
232
+ if active:
233
+ selected.remove(tab)
234
+ else:
235
+ selected.append(tab)
236
+ else:
237
+ selected = [tab]
238
+
239
+ same_line()
240
+
241
+ imgui.dummy(0, 0)
242
+
243
+ if changed:
244
+ Core.melty.refresh_nested_windows(draw_state)
245
+
246
+ return changed, selected
247
+
248
+
249
+ @render_func(with_header=draw_header, is_tree=False, shadow=False)
250
+ def draw_enum_tabs(input_value: type, tab_state: TabState):
251
+ enum_states = list(input_value)
252
+ selected = tab_state.selected_tabs
253
+
254
+ changed, new_selected = draw_tab_bar(selected, collection=enum_states)
255
+ if changed:
256
+ tab_state.selected_tabs = new_selected
257
+
258
+ return False, input_value
@@ -0,0 +1,430 @@
1
+ """Tensor view functions and supporting definitions."""
2
+ from meltygui.hdr_color import pack_color
3
+ from meltygui.state.tensor_state import TensorErrorState
4
+ from meltygui.model.tensor_model import TensorDim
5
+ from meltygui.model.tensor_model import TensorDims
6
+ from meltygui.core.core_render import render_func
7
+ from meltygui.core.rendering.render_funcs import RenderFuncs
8
+ from meltygui.view.header_view import draw_header
9
+ import math
10
+ import meltygui_imgui as imgui
11
+ from meltygui.view.header_view import flat_button
12
+ from meltygui.view.decoration_view import draw_bg
13
+
14
+
15
+ @render_func(tint=(0.23, 0.49, 0.62), use_cache=True, show_bg=False,
16
+ with_header=None, show_header=False, shadow=False, auto_resize=True, wrap=False)
17
+ def draw_tensor_slices(input_value: tuple, slider_dims=(), dim_names=(),
18
+ source_shape=(), draw_state=None):
19
+ """Edit unmapped tensor dimensions through ordinary integer controls."""
20
+ from meltygui.view.control_view import draw_int_slider
21
+ values = list(input_value) + [0] * max(0, len(source_shape) - len(input_value))
22
+ changed = False
23
+ for dimension in slider_dims:
24
+ label = dim_names[dimension] if dimension < len(dim_names) else f"dim{dimension}"
25
+ upper_bound = max(0, source_shape[dimension] - 1)
26
+ current = max(0, min(int(values[dimension]), upper_bound))
27
+ edited, value = draw_int_slider(current, name=f"{label}##slice{dimension}",
28
+ min_value=0, max_value=upper_bound,
29
+ width=draw_state.content_width, height=22)
30
+ if edited and value != current:
31
+ values[dimension] = value
32
+ changed = True
33
+ return changed, tuple(values) if changed else input_value
34
+
35
+
36
+ def _draw_slice_sliders(draw_state, slider_dims, dim_names, slices, source_shape, width):
37
+ changed, value = draw_tensor_slices(slices, slider_dims=slider_dims,
38
+ dim_names=dim_names, source_shape=source_shape,
39
+ name="Slice positions", width=width)
40
+ if changed:
41
+ draw_state.locate_slices = value
42
+ return changed, value
43
+
44
+
45
+ @render_func(tint=(0.68, 0.22, 0.20), use_cache=True, show_bg=False,
46
+ with_header=None, show_header=False, shadow=False, auto_resize=False)
47
+ def draw_tensor_error(input_value: str, draw_state=None,
48
+ error_state: TensorErrorState = None, who="draw_voxels"):
49
+ """An error card with the same footprint as a tensor or graph view."""
50
+ import textwrap
51
+ left, top, right, bottom = draw_state.get_content_rect()
52
+ width, height = right - left, bottom - top
53
+ padding = 12.0
54
+ draw_list = imgui.get_window_draw_list()
55
+ draw_list.add_rect_filled(left, top, left + width, top + height,
56
+ pack_color(0.09, 0.05, 0.05, 1.0), 6.0)
57
+ draw_list.add_rect(left, top, left + width, top + height,
58
+ pack_color(0.75, 0.25, 0.25, 0.9), 6.0, thickness=1.5)
59
+ draw_list.add_text(left + padding, top + padding,
60
+ pack_color(1.0, 0.55, 0.55, 1.0),
61
+ f"{who} can't display this tensor")
62
+ line_height = imgui.get_font_size() + 2
63
+ line_top = top + padding + line_height + 4
64
+ columns = max(1, int((width - padding * 2) / max(1, imgui.calc_text_size("M")[0])))
65
+ draw_list.push_clip_rect(left + padding, top + padding, right - padding, bottom - padding)
66
+ for paragraph in input_value.splitlines():
67
+ for line in textwrap.wrap(paragraph, width=columns) or ['']:
68
+ draw_list.add_text(left + padding, line_top, pack_color(1.0, 0.85, 0.85, 1.0), line)
69
+ line_top += line_height
70
+ draw_list.pop_clip_rect()
71
+ imgui.dummy(width, height)
72
+ if error_state.last_error != input_value:
73
+ error_state.last_error = input_value
74
+ print(f"[{who}] {draw_state.name}: {input_value}")
75
+ return False, input_value
76
+
77
+
78
+ def _draw_voxel_error(draw_state, message, who="draw_voxels"):
79
+ width, height = _view_size(draw_state)
80
+ return draw_tensor_error(message, name=f"{who} error", who=who,
81
+ width=width, height=height)
82
+
83
+
84
+ @render_func(is_default_for=("TensorDim", "TensorDims"), show_bg=False, is_tree=False,
85
+ header_same_line=True, with_header=draw_header)
86
+ def draw_tensor_dim(input_value: TensorDim | TensorDims = None, draw_state=None,
87
+ unique=0, ui_scale=1.0, style_manager=None, **kwargs):
88
+ """THE dim picker — one TAB per dim NAME instead of a bare number field,
89
+ shared by every dim-typed param. A TensorDim renders single-select with
90
+ a leading "off" tab that maps to -1 (unset: sort disabled, nf/axis dims
91
+ derived), so sort_dim and nf_chop/nf_along reuse it as-is. A TensorDims
92
+ renders the same tabs multi-select (mean_dims). The tabs are this view's
93
+ own flat_buttons (`_draw_dim_tabs`), not a nested draw_tab_bar. Picking
94
+ a dim a sibling AXIS row already holds swaps the two (`_swap_sibling_dim`,
95
+ SWAP_DIM_KEYS). The names
96
+ come from the sibling `dim_names` entry of the collection this row
97
+ renders in (the params panel's locate_params proxy); with no names in
98
+ reach it falls back to a plain int edit. Returns the SAME type it was
99
+ given so the value keeps routing here (a plain int/tuple would drop back
100
+ to the generic renderer next frame)."""
101
+ from meltygui.model.tensor_model import TensorDim
102
+ from meltygui.model.tensor_model import TensorDims
103
+ from meltygui.model.tensor_model import _collection_dim_labels
104
+
105
+ multi = isinstance(input_value, tuple)
106
+ col = _row_collection(draw_state, kwargs)
107
+ labels = _collection_dim_labels(col)
108
+ if not labels:
109
+ if multi:
110
+ imgui.text(f"dims: {tuple(int(v) for v in input_value)}")
111
+ return False, input_value
112
+ changed, v = RenderFuncs.draw_int(
113
+ 0 if input_value is None else int(input_value),
114
+ name=f"dim##{unique}")
115
+ if changed and v is not None:
116
+ return True, TensorDim(int(v))
117
+ return False, input_value
118
+ n = len(labels)
119
+ if multi:
120
+ cur = [int(v) for v in input_value if isinstance(v, int)]
121
+ changed, selected = _draw_dim_tabs(
122
+ draw_state, list(range(n)), labels,
123
+ [d for d in cur if 0 <= d < n], multi=True,
124
+ ui_scale=ui_scale, style_manager=style_manager)
125
+ if changed:
126
+ return True, TensorDims(sorted(int(s) for s in selected))
127
+ return False, input_value
128
+ # Single-select: a leading "off" tab maps to -1 (unset - sort disabled,
129
+ # nf/axis dims derived), so unsetting doesn't rely on double-click.
130
+ cur = int(input_value) if input_value is not None else -1
131
+ if not (0 <= cur < n):
132
+ cur = -1
133
+ changed, selected = _draw_dim_tabs(
134
+ draw_state, [-1] + list(range(n)), ["off"] + labels, [cur], multi=False,
135
+ ui_scale=ui_scale, style_manager=style_manager)
136
+ if changed:
137
+ new = int(selected[0]) if selected else -1
138
+ _swap_sibling_dim(draw_state, kwargs, cur, new)
139
+ return True, TensorDim(new)
140
+ return False, input_value
141
+
142
+
143
+ def _describe_tensor(t):
144
+ """'Tensor[32, 1, 570, 4096] float16 cuda:0' — for error cards."""
145
+ try:
146
+ dev = getattr(t, "device", None)
147
+ dev = f" {dev}" if dev is not None and str(dev) != "cpu" else ""
148
+ return f"{type(t).__name__}{list(t.shape)} {str(t.dtype).replace('torch.', '')}{dev}"
149
+ except Exception:
150
+ return repr(t)[:80]
151
+
152
+
153
+ def _view_size(draw_state):
154
+ """(width, height) the 3-D image occupies — shared by the render path
155
+ and the error card so the card holds the view's footprint (no layout
156
+ jump between a good frame and a failed one). Sized from the OWNING
157
+ WINDOW, not this view's own content rect — a nested view's rect derives
158
+ from what it rendered last frame (self-referential), while the window's
159
+ height is the user-dragged size. The owning window IS the draw_state
160
+ when draw_voxels is itself a window (closable); parent_window for a
161
+ closable voxel grabbed an ANCESTOR that doesn't move with it."""
162
+ win = draw_state if draw_state.closable else (draw_state.parent_window or draw_state)
163
+ width = max(64, int(draw_state.content_width or win.content_width or 0))
164
+ # Vertical reserve: the actual header band (0 if hidden) plus a little
165
+ # slack for the footer/status margin (the old hardcoded 30 was the
166
+ # 23px header + this slack).
167
+ _reserve = int(draw_state.header_height or 0) + 7
168
+ if draw_state.closable:
169
+ height = max(100, draw_state.height - _reserve)
170
+ else:
171
+ # When in a parent's flow, draw_state.height is only trustworthy
172
+ # when something explicit wrote it - a user drag ("initial
173
+ # window size"), a passed height kwarg, fill_height. The auto_resize
174
+ # measurement path ("... item_rect[1]") is what this view drew last
175
+ # frame - sizing the image from it is a feedback loop that sustains
176
+ # any spike forever (image = height-30 → measures back ≈ height →
177
+ # committed again); fall back to the design height (min_height,
178
+ # overridable per call site) for that case, and a bad committed
179
+ # height self-heals on the next live render.
180
+ _h_src = str(draw_state._source.get("height", ""))
181
+ if draw_state.height and "item_rect" not in _h_src:
182
+ height = max(100, int(draw_state.height) - _reserve)
183
+ else:
184
+ height = max(100, int(draw_state.min_height or 293) - _reserve)
185
+ return width, height
186
+
187
+
188
+ def _draw_image_notice(img_pos, width, text):
189
+ """Small wrapped caption on the image's top-left (clamp notices):
190
+ a translucent plate under wrapped imgui text, cursor restored after."""
191
+ x, y = img_pos
192
+ pad = 6.0
193
+ wrap_w = max(40.0, width - 2 * pad - 4)
194
+ tw, th = imgui.calc_text_size(text, wrap_width=wrap_w)
195
+ imgui.get_window_draw_list().add_rect_filled(
196
+ x + 2, y + 2, x + min(tw, wrap_w) + 2 * pad + 2, y + th + 2 * pad + 2,
197
+ pack_color(0.0, 0.0, 0.0, 0.6), 4.0)
198
+ cur = imgui.get_cursor_screen_pos()
199
+ imgui.set_cursor_screen_pos((x + pad + 2, y + pad + 2))
200
+ imgui.push_text_wrap_pos(x + pad + 2 + wrap_w)
201
+ imgui.push_style_color(imgui.COLOR_TEXT, 1.0, 0.8, 0.4, 1.0)
202
+ imgui.text_wrapped(text)
203
+ imgui.pop_style_color()
204
+ imgui.pop_text_wrap_pos()
205
+ imgui.set_cursor_screen_pos(cur)
206
+
207
+
208
+ def format_bytes(n):
209
+ """Tensor byte count → 'KB' / 'MB' / 'GB' string (the old volume
210
+ renderer's 2-dp formatting) plus its size tint: green ≤10 MB, yellow
211
+ ≤100 MB, red above."""
212
+ n = int(n or 0)
213
+ if n > 1024 ** 3:
214
+ txt = f"{n / 1024 ** 3:.2f} GB"
215
+ elif n > 1024 ** 2:
216
+ txt = f"{n / 1024 ** 2:.2f} MB"
217
+ else:
218
+ txt = f"{n / 1024:.2f} KB"
219
+ if n > 100 * 1024 ** 2:
220
+ tint = (1.0, 0.45, 0.4, 1.0)
221
+ elif n > 10 * 1024 ** 2:
222
+ tint = (1.0, 0.85, 0.35, 1.0)
223
+ else:
224
+ tint = (0.5, 0.9, 0.5, 1.0)
225
+ return txt, tint
226
+
227
+
228
+ def _draw_tensor_meta(img_pos, height, t):
229
+ """Bottom-left caption over the image: shape · dtype · device · bytes
230
+ (size tinted by magnitude), read straight off the source tensor."""
231
+ try:
232
+ shape = "×".join(str(int(d)) for d in t.shape)
233
+ dtype = str(t.dtype).replace("torch.", "")
234
+ nbytes = int(t.numel()) * int(t.element_size())
235
+ except Exception:
236
+ return
237
+ dev = str(getattr(t, "device", "cpu"))
238
+ head = " ".join(p for p in (shape, dtype, dev if dev != "cpu" else "") if p)
239
+ size_txt, size_tint = format_bytes(nbytes)
240
+ pad, gap = 5.0, 8.0
241
+ hw, hh = imgui.calc_text_size(head)
242
+ sw, sh = imgui.calc_text_size(size_txt)
243
+ tw, th = hw + gap + sw, max(hh, sh)
244
+ x, y = img_pos
245
+ x0, y0 = x + 2, y + height - th - 2 * pad - 2
246
+ dl = imgui.get_window_draw_list()
247
+ dl.add_rect_filled(x0, y0, x0 + tw + 2 * pad, y0 + th + 2 * pad,
248
+ pack_color(0.0, 0.0, 0.0, 0.55), 4.0)
249
+ dl.add_text(x0 + pad, y0 + pad, pack_color(0.85, 0.85, 0.85, 1.0), head)
250
+ dl.add_text(x0 + pad + hw + gap, y0 + pad, pack_color(*size_tint), size_txt)
251
+
252
+
253
+ DIM_TAB_HEIGHT = 26
254
+
255
+
256
+ DIM_TAB_TEXT_PAD = 11
257
+
258
+
259
+ DIM_TAB_GAP = 3
260
+
261
+
262
+ DIM_TAB_BAND_PAD = 3
263
+
264
+
265
+ DIM_TAB_COLOR = (0.5, 0.5, 0.5)
266
+
267
+
268
+ SWAP_DIM_KEYS = frozenset({"x_dim", "y_dim", "z_dim", "line_dim"})
269
+
270
+
271
+ def _row_collection(draw_state, kwargs):
272
+ """The collection this row renders in (the params panel's locate_params
273
+ proxy) — sibling params like dim_names / x_dim live there."""
274
+ col = kwargs.get("collection")
275
+ if not isinstance(col, dict):
276
+ col = getattr(draw_state, "_collection", None)
277
+ return col if isinstance(col, dict) else None
278
+
279
+
280
+ def _draw_dim_tabs(draw_state, options, labels, selected, multi, *,
281
+ ui_scale=1.0, style_manager=None):
282
+ """The dim tab strip: one flat_button per option, laid out by hand
283
+ (wrapping at draw_state.content_width) over the same draw_bg band
284
+ draw_tab_bar's wrapper painted (bg_offset=-3, rounding 5), with the tab
285
+ bar's look — active = filled rect + shadow, inactive = label only, hover
286
+ brightens — but DIM_TAB_* geometry. No nested render_func: the buttons
287
+ paint straight into this view's draw list and claim their clicks through
288
+ this view's draw_state.on_action, so the row costs one wrapper instead
289
+ of two. Returns (changed, selected) with `selected` a list of option
290
+ values; multi toggles, single replaces."""
291
+ tab_h = DIM_TAB_HEIGHT * ui_scale
292
+ pad = DIM_TAB_TEXT_PAD * ui_scale
293
+ gap = DIM_TAB_GAP * ui_scale
294
+ band_pad = DIM_TAB_BAND_PAD * ui_scale
295
+ x0, y0 = imgui.get_cursor_screen_pos()
296
+ content_width = draw_state.content_width if draw_state is not None else 0
297
+ x_limit = x0 + content_width if content_width > 0 else None
298
+ # Layout first (pure math over the label widths) so the band can be
299
+ # painted UNDER the buttons without carrying last frame's extent on the
300
+ # draw_state the way the tab / show_bg path does.
301
+ rects = []
302
+ x, y = x0 + band_pad, y0 + band_pad
303
+ for label in labels:
304
+ w = imgui.calc_text_size(label).x + pad
305
+ if rects and x_limit is not None and x + w + band_pad > x_limit:
306
+ x, y = x0 + band_pad, y + tab_h + gap
307
+ rects.append((x, y, w))
308
+ x += w + gap
309
+ right = max(rx + rw for rx, _, rw in rects) + band_pad
310
+ bottom = rects[-1][1] + tab_h + band_pad
311
+ draw_bg(left=x0, top=y0, width=right - x0, height=bottom - y0,
312
+ rounding=5, bg_offset=-3, depth=draw_state.depth_and_layer[0], opacity=1.0,
313
+ selected=False, pressed=False, nested_bg=False,
314
+ style_manager=style_manager)
315
+ changed = False
316
+ selected = list(selected)
317
+ for i, (opt, label, (x, y, w)) in enumerate(zip(options, labels, rects)):
318
+ imgui.set_cursor_screen_pos((x, y))
319
+ active = opt in selected
320
+ # Same params draw_tab_bar hands flat_button for an untinted tab
321
+ # (new_value=0.15, factor=1.2): active keeps flat_button's full
322
+ # text/saturation, inactive is alpha=0 with the muted text.
323
+ if active:
324
+ clicked = flat_button(label, draw_state, view_id=f"dim_tab_{i}",
325
+ width=w, height=tab_h, color=DIM_TAB_COLOR,
326
+ factor=1.2, tint_value=0.15 + 0.23 - 0.03)
327
+ else:
328
+ clicked = flat_button(label, draw_state, view_id=f"dim_tab_{i}",
329
+ width=w, height=tab_h, color=DIM_TAB_COLOR,
330
+ factor=1.2, alpha=0.0, tint_value=0.15,
331
+ saturation=0.3, text_value=1.0)
332
+ if clicked:
333
+ changed = True
334
+ if multi:
335
+ if active:
336
+ selected.remove(opt)
337
+ else:
338
+ selected.append(opt)
339
+ else:
340
+ selected = [opt]
341
+ # Register the whole band (pads included) with the layout so the row's
342
+ # measured extent covers it, and leave the cursor below it.
343
+ imgui.set_cursor_screen_pos((x0, y0))
344
+ imgui.dummy(right - x0, bottom - y0)
345
+ return changed, selected
346
+
347
+
348
+ def _sibling_dim_keys(draw_state):
349
+ """Keys of the OTHER draw_tensor_dim rows in the panel this row renders
350
+ in, found by walking the render tree (the parent's _view_children index,
351
+ where every rendered child self-registers) rather than the collection:
352
+ what is actually rendering as a dim picker right now, whatever the
353
+ collection stores. Read-only over the siblings — no value ever moves
354
+ through another row's draw_state."""
355
+ parent = draw_state._parent if draw_state is not None else None
356
+ if parent is None or parent is draw_state: # a root ds parents itself
357
+ return []
358
+ keys = []
359
+ for sib in parent._view_children.values():
360
+ if sib is None or sib is draw_state or sib._parent is not parent:
361
+ continue
362
+ # Recognize the renderer by its stable name, including rows restored
363
+ # from older sessions or decorated through another call path.
364
+ if getattr(sib._view_func, "__name__", None) != "draw_tensor_dim":
365
+ continue
366
+ key = (sib._kwargs or {}).get("key")
367
+ if key is not None:
368
+ keys.append(key)
369
+ return keys
370
+
371
+
372
+ def _swap_sibling_dim(draw_state, kwargs, old, new):
373
+ """This row just moved from `old` to `new`: if a sibling AXIS row holds
374
+ `new`, hand it `old` so the two axes swap. The hand-off is the same
375
+ write draw_collection performs when that row returns changed —
376
+ `collection[key] = value` on the panel's ParamProxy (→ set_anywhere) —
377
+ so the sibling's value takes the normal route whether or not that row
378
+ renders this frame, and nothing is posted on its draw_state."""
379
+ if new < 0:
380
+ return # "off" can be shared freely
381
+ key = (draw_state._kwargs or {}).get("key") if draw_state is not None else None
382
+ if key not in SWAP_DIM_KEYS:
383
+ return
384
+ col = _row_collection(draw_state, kwargs)
385
+ if col is None:
386
+ return
387
+ for sib_key in _sibling_dim_keys(draw_state):
388
+ if sib_key not in SWAP_DIM_KEYS or sib_key not in col:
389
+ continue
390
+ v = col.get(sib_key)
391
+ if isinstance(v, int) and not isinstance(v, bool) and int(v) == new:
392
+ col[sib_key] = TensorDim(old)
393
+ return
394
+
395
+
396
+ def _tick_values(lo, hi, px_per_idx, num_px, spacing=1.6):
397
+ """Integer tick positions for the VISIBLE [lo, hi] index span of one
398
+ edge: EVERY integer when the labels fit, else the smallest 1-2-5·10ᵏ
399
+ step whose rotated labels keep clear of each other (footprint ≈ the
400
+ widest label's text width along the edge, in projected PIXELS — so
401
+ zooming in fits more ticks). `spacing` is the minimum gap between tick
402
+ centers in widest-label widths. The span's end values always show —
403
+ 0/max on an unclipped edge, the boundary indices (a scrollbar-like
404
+ readout of where you are along the axis) on a clipped one; interior
405
+ step multiples stay GLOBAL multiples (they don't jitter as the clip
406
+ end moves) and yield when they would crowd an end."""
407
+ e0, e1 = int(math.ceil(lo - 1e-9)), int(math.floor(hi + 1e-9))
408
+ if e1 < e0:
409
+ return []
410
+ if e1 == e0:
411
+ return [e0]
412
+ widest = max(1, len(str(e1))) * 0.62 * num_px # ~max glyph aspect
413
+ min_px = widest * spacing
414
+ step, k = None, 1
415
+ while step is None and k <= 10 ** 9:
416
+ for s in (1, 2, 5):
417
+ if s * k * px_per_idx >= min_px:
418
+ step = s * k
419
+ break
420
+ else:
421
+ k *= 10
422
+ if step is None or step > e1 - e0:
423
+ return [e0, e1]
424
+ ticks = [e0]
425
+ m = int(math.ceil((e0 + 0.6 * step) / step)) * step
426
+ while m <= e1 - 0.6 * step:
427
+ ticks.append(m)
428
+ m += step
429
+ ticks.append(e1)
430
+ return ticks