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,706 @@
1
+ import functools as _functools
2
+ import gc as _gc
3
+ import sys as _sys
4
+ import locale
5
+ import threading as _threading
6
+ import time
7
+ from collections import deque, defaultdict
8
+ from contextlib import contextmanager as _contextmanager
9
+
10
+ import meltygui.core.windowing.window_api as glfw
11
+ import meltygui_imgui as imgui
12
+ from meltygui.hdr_color import pack_color
13
+
14
+ from meltygui.core.styling.fonts import Font
15
+ from meltygui.core.runtime.toggles import Toggles
16
+
17
+ # Use the system's locale time format (e.g. 12-hour AM/PM if configured)
18
+ # for %X instead of the default "C" locale 24-hour clock.
19
+ try:
20
+ locale.setlocale(locale.LC_TIME, "")
21
+ except locale.Error:
22
+ pass
23
+
24
+
25
+ def _timestamp():
26
+ """Locale-aware time string (AM/PM on systems configured for 12-hour)."""
27
+ return time.strftime("%X", time.localtime())
28
+
29
+
30
+ def _fade_opacity(created_at):
31
+ """Opacity that decays with age: 100% fresh, 50% at ~10s, floored at 30%."""
32
+ elapsed = time.time() - created_at
33
+ return max(0.3, 1.0 - 0.05 * elapsed)
34
+
35
+ class NotificationCenter:
36
+ max_notifications = 20
37
+ notifications = deque(maxlen=max_notifications)
38
+ tagged_notifications = defaultdict(lambda: deque(maxlen=NotificationCenter.max_notifications))
39
+ ignore_tags = ["host"]
40
+ # Single live value per tag (overwritten on each display() call), shown in
41
+ # the dedicated "Live" column. Insertion order = row order.
42
+ live_values = {}
43
+
44
+
45
+ class Notification:
46
+ """One toast. `stack` is an optional list of (filename, lineno, funcname)
47
+ tuples, OUTERMOST FIRST (the capture_stack() shape); `jump` is the
48
+ (path, line) the entry opens in the code editor when clicked — by default
49
+ the innermost stack frame. `count` is how many times the same (tag, text,
50
+ stack) was notified while this entry was in the column."""
51
+ __slots__ = ("text", "tint", "time_label", "created_at", "stack", "jump", "count")
52
+
53
+ def __init__(self, text, tint, time_label, created_at, stack=None, jump=None):
54
+ self.text = text
55
+ self.tint = tint
56
+ self.time_label = time_label
57
+ self.created_at = created_at
58
+ self.stack = stack
59
+ self.jump = jump
60
+ self.count = 1
61
+
62
+ # Legacy 4-tuple unpacking (text, tint, time_label, created_at).
63
+ def __iter__(self):
64
+ return iter((self.text, self.tint, self.time_label, self.created_at))
65
+
66
+ @property
67
+ def key(self):
68
+ return (self.text, self.jump, tuple(self.stack) if self.stack else None)
69
+
70
+ @property
71
+ def label(self):
72
+ """Text as drawn: the message, a repeat count, and the jump site."""
73
+ out = self.text if self.count == 1 else f"{self.text} ({self.count})"
74
+ if self.jump is not None:
75
+ path, line = self.jump
76
+ out += f"\n{_short_path(path)}:{line}"
77
+ return out
78
+
79
+ @property
80
+ def copy_text(self):
81
+ """Clipboard form: the label plus the whole captured stack."""
82
+ out = self.label
83
+ if self.stack:
84
+ out += "\n" + "\n".join(f' File "{f}", line {ln}, in {fn}' for f, ln, fn in self.stack)
85
+ return out
86
+
87
+
88
+ def _short_path(path):
89
+ path = str(path)
90
+ marker = "/src/"
91
+ i = path.rfind(marker)
92
+ return path[i + 1:] if i >= 0 else path.rsplit("/", 1)[-1]
93
+
94
+
95
+ def capture_stack(skip_files=(), skip_funcs=(), limit=12):
96
+ """Cheap stack capture for notify(stack=...): walks live frames with
97
+ sys._getframe (no source-line lookup, unlike traceback.extract_stack) and
98
+ returns [(filename, lineno, funcname), ...] OUTERMOST FIRST, at most
99
+ `limit` frames. Frames whose file ends with an entry of `skip_files` or
100
+ whose function name is in `skip_funcs` are dropped from the INNER end
101
+ until the first frame that is neither — so the innermost kept frame is
102
+ the "caller" of the skipped plumbing. Returns [] off the interpreter."""
103
+ f = _sys._getframe(1)
104
+ out = []
105
+ skipping = True
106
+ while f is not None and len(out) < limit:
107
+ code = f.f_code
108
+ fn = code.co_filename
109
+ if skipping and (fn.endswith(tuple(skip_files)) or code.co_name in skip_funcs
110
+ or fn.endswith("notifications.py")):
111
+ f = f.f_back
112
+ continue
113
+ skipping = False
114
+ out.append((fn, f.f_lineno, code.co_name))
115
+ f = f.f_back
116
+ out.reverse()
117
+ return out
118
+
119
+
120
+ def notify(text, tint=(1,1,1,1), tag=None, urgent=False, stack=None, jump=None):
121
+ """Push a toast into the `tag` column. `stack` (see capture_stack) makes
122
+ the entry clickable: clicking opens `jump` — default the innermost stack
123
+ frame — in the code editor. Repeats of the same (tag, text, stack) while
124
+ the previous entry is still in the column bump its count (drawn as
125
+ "(3)") and refresh its time instead of adding a row."""
126
+ formatted_time = _timestamp()
127
+ created_at = time.time()
128
+ tint = tint if len(tint) == 4 else (*tint, 1) # Ensure color has alpha
129
+ if stack:
130
+ stack = [tuple(fr[:3]) for fr in stack]
131
+ if jump is None:
132
+ jump = (stack[-1][0], stack[-1][1])
133
+ if jump is not None:
134
+ jump = (str(jump[0]), int(jump[1]))
135
+
136
+ column = NotificationCenter.tagged_notifications["General" if tag is None else tag]
137
+ entry = Notification(text, tint, formatted_time, created_at, stack=stack, jump=jump)
138
+ for existing in column:
139
+ if existing.key == entry.key:
140
+ existing.count += 1
141
+ existing.tint = tint
142
+ existing.time_label = formatted_time
143
+ existing.created_at = created_at
144
+ column.remove(existing)
145
+ column.appendleft(existing)
146
+ break
147
+ else:
148
+ column.appendleft(entry)
149
+
150
+ if urgent:
151
+ from meltygui.core.windowing.glfw_utils import request_render
152
+ request_render()
153
+
154
+
155
+ @_contextmanager
156
+ def lag_span(label, min_ms=50.0):
157
+ """Notify (tag "lag") when the wrapped block ran slower than `min_ms` —
158
+ near-zero cost when fast, so it can sit on hot paths. Includes the thread
159
+ name: a slow span on a worker still stalls the render thread for its
160
+ GIL-held portion, so every entry here is a frame-drop suspect."""
161
+ # Stack of the SPAN SITE, captured on entry (a few µs - no source
162
+ # lookup); clicking the toast opens the `with lag_span(...)` line. Skips
163
+ # contextlib's generator plumbing and lag_traced's wrapper.
164
+ stack = capture_stack(skip_files=("contextlib.py",), skip_funcs=("wrapper", "lag_span"))
165
+ t0 = time.perf_counter()
166
+ try:
167
+ yield
168
+ finally:
169
+ ms = (time.perf_counter() - t0) * 1000.0
170
+ if ms >= min_ms:
171
+ tint = (1.0, 0.25, 0.2) if ms >= 300 else (1.0, 0.65, 0.2)
172
+ notify(f"{label} {ms:.0f}ms [{_threading.current_thread().name}]",
173
+ tint=tint, tag="lag", stack=stack)
174
+
175
+
176
+ def lag_traced(label, min_ms=50.0):
177
+ """Decorator form of lag_span — stamp on any function suspected of
178
+ GIL-held stalls; it reports only when a call actually ran slow."""
179
+ def deco(fn):
180
+ @_functools.wraps(fn)
181
+ def wrapper(*args, **kwargs):
182
+ with lag_span(label, min_ms=min_ms):
183
+ return fn(*args, **kwargs)
184
+ return wrapper
185
+ return deco
186
+
187
+
188
+ # GC watch: a generation-2 collection walks every live object with the GIL
189
+ # held - with several large cst dicts resident that's an intermittent multi-
190
+ # hundred-ms stall attributed to whichever thread happened to allocate. The
191
+ # lazy install keeps a hotswap re-exec (which reuses module globals) from
192
+ # stacking callbacks.
193
+ _gc_start = {}
194
+
195
+
196
+ def _gc_watch(phase, info):
197
+ gen = info.get("generation")
198
+ if phase == "start":
199
+ _gc_start[gen] = time.perf_counter()
200
+ return
201
+ t0 = _gc_start.pop(gen, None)
202
+ if t0 is not None:
203
+ ms = (time.perf_counter() - t0) * 1000.0
204
+ if ms >= 20.0:
205
+ # urgent=False: a gc callback can fire at ANY allocation point,
206
+ # including mid-operation at boot - never pull request_render (and
207
+ # its lazy glfw import) from here; the next UI frame shows it.
208
+ try:
209
+ # The stack is whatever allocation the collector interrupted -
210
+ # still the best "who triggered it" there is.
211
+ notify(f"gc gen{gen} {ms:.0f}ms collected={info.get('collected')}"
212
+ f" [{_threading.current_thread().name}]",
213
+ tint=(1.0, 0.25, 0.2) if ms >= 300 else (1.0, 0.65, 0.2),
214
+ tag="lag", urgent=False, stack=capture_stack(skip_funcs=("_gc_watch",)))
215
+ except Exception:
216
+ pass
217
+
218
+
219
+ # One installed callback per PROCESS, tracked on sys: a module-global guard
220
+ # resets on the in-place restart's re-import, and each stacked callback pinned
221
+ # its session's module dict (and fired on every GC). Replace, don't append.
222
+ # (Sweeping by name also sheds the stack left by sessions predating this.)
223
+ _gc.callbacks[:] = [cb for cb in _gc.callbacks
224
+ if getattr(cb, "__name__", None) != "_gc_watch"]
225
+ _gc.callbacks.append(_gc_watch)
226
+ _sys._lsd_gc_watch = _gc_watch
227
+
228
+
229
+ def _format_value(value):
230
+ """Render a variety of value types into a compact display string."""
231
+ # scalar tensors / numpy scalars into a plain python number
232
+ item = getattr(value, "item", None)
233
+ if callable(item):
234
+ try:
235
+ value = value.item()
236
+ except Exception:
237
+ return str(value)
238
+ if isinstance(value, bool):
239
+ return str(value)
240
+ if isinstance(value, float):
241
+ return f"{value:.4g}"
242
+ return str(value)
243
+
244
+
245
+ def display(value, tint=(1, 1, 1, 1), tag=None, urgent=True):
246
+ """Show a single live value in the "Live" column, keyed by `tag`.
247
+
248
+ Unlike notify(), which keeps a running list per tag, display() overwrites the
249
+ value for a tag each call so it updates in place over time."""
250
+ formatted_time = _timestamp()
251
+ created_at = time.time()
252
+ tint = tint if len(tint) == 4 else (*tint, 1) # Ensure color has alpha
253
+ key = tag if tag is not None else "value"
254
+ NotificationCenter.live_values[key] = (_format_value(value), tint, formatted_time, created_at)
255
+
256
+ # if urgent:
257
+ # from meltygui.core.windowing.glfw_utils import request_render
258
+ # request_render()
259
+
260
+
261
+ def _wrap_text(text, max_width):
262
+ """Greedy word-wrap `text` into lines no wider than `max_width` px.
263
+ Preserves explicit newlines. A single word wider than the limit is left on
264
+ its own line (it overflows rather than being split mid-word)."""
265
+ if max_width <= 0:
266
+ return [text]
267
+ # Memoized per (text, width): every visible toast re-wraps its text
268
+ # twice a frame (height measure + draw), each a calc_text_size per word.
269
+ memo_key = (text, max_width)
270
+ hit = _WRAP_MEMO.get(memo_key)
271
+ if hit is not None:
272
+ return hit
273
+ lines = []
274
+ for paragraph in text.split("\n"):
275
+ current = ""
276
+ for word in paragraph.split(" "):
277
+ candidate = word if not current else current + " " + word
278
+ if not current or imgui.calc_text_size(candidate).x <= max_width:
279
+ current = candidate
280
+ else:
281
+ lines.append(current)
282
+ current = word
283
+ lines.append(current)
284
+ if len(_WRAP_MEMO) > 1024:
285
+ _WRAP_MEMO.clear()
286
+ _WRAP_MEMO[memo_key] = lines
287
+ return lines
288
+
289
+
290
+ _WRAP_MEMO = globals().get("_WRAP_MEMO", {}) # (text, max_width) → lines
291
+
292
+
293
+ def _entry_height(label, content, content_width, line_height, padding):
294
+ """Vertical space one stacked entry consumes: wrapped block + bg padding +
295
+ the inter-entry gap (matches _draw_column_entry's bg_top - padding step)."""
296
+ label_size = imgui.calc_text_size(label)
297
+ lines = _wrap_text(content, content_width - (label_size.x + padding))
298
+ return max(1, len(lines)) * line_height + padding * 3
299
+
300
+
301
+ def _draw_column_entry(draw_list, column_left, content_width, line_height, padding,
302
+ bg_bottom, label, label_color, content, content_color, opacity=1.0,
303
+ hit_rects=None, copy_text=None, jump=None, clip=None):
304
+ """Draw one stacked entry: a left-aligned `label` (time / tag name) followed
305
+ by the wrapped, tinted `content`. `label_color`/`content_color` are RGBA
306
+ tuples; `opacity` scales every alpha so the whole toast fades with age.
307
+ When `hit_rects` is given, appends (rect, copy_text, jump) so the caller
308
+ can make the entry click-to-copy (or click-to-jump when `jump` is a
309
+ (path, line)); `clip` (a scroll viewport rect) clamps the hit rect so the
310
+ scrolled-out part of an entry can't swallow clicks. Returns the next
311
+ bg_bottom (above this one)."""
312
+ label_size = imgui.calc_text_size(label)
313
+ content_x = column_left + label_size.x + padding
314
+
315
+ lines = _wrap_text(content, content_width - (label_size.x + padding))
316
+ block_height = max(1, len(lines)) * line_height
317
+ bg_top = bg_bottom - (block_height + padding * 2)
318
+ content_top = bg_top + padding
319
+
320
+ # The entry under the mouse is drawn at full opacity regardless of age.
321
+ mouse = imgui.get_io().mouse_pos
322
+ if (column_left - padding <= mouse.x <= column_left + content_width + padding
323
+ and bg_top <= mouse.y <= bg_bottom):
324
+ opacity = 1.0
325
+
326
+ label_u32 = pack_color(label_color[0], label_color[1],
327
+ label_color[2], label_color[3] * opacity)
328
+ content_u32 = pack_color(content_color[0], content_color[1],
329
+ content_color[2], content_color[3] * opacity)
330
+
331
+ rect = (column_left - padding, bg_top, column_left + content_width + padding, bg_bottom)
332
+
333
+ # background rectangle with some transparency
334
+ draw_list.add_rect_filled(*rect, pack_color(0, 0, 0, opacity), rounding=2)
335
+
336
+ if hit_rects is not None:
337
+ hit_rect = rect if clip is None else (max(rect[0], clip[0]), max(rect[1], clip[1]),
338
+ min(rect[2], clip[2]), min(rect[3], clip[3]))
339
+ if hit_rect[0] < hit_rect[2] and hit_rect[1] < hit_rect[3]:
340
+ hit_rects.append((hit_rect, content if copy_text is None else copy_text, jump))
341
+
342
+ # label on the first line, then the wrapped, left-aligned content
343
+ draw_list.add_text(column_left, content_top, label_u32, label)
344
+ for li, line in enumerate(lines):
345
+ draw_list.add_text(content_x, content_top + li * line_height, content_u32, line)
346
+
347
+ return bg_top - padding
348
+
349
+
350
+ # Tags whose band is collapsed to its title strip (toggled by the tree arrow
351
+ # beside the title, see _draw_column_title / _handle_arrow_click). Hotswap-safe.
352
+ _collapsed_tags = globals().get("_collapsed_tags") or set()
353
+
354
+
355
+ def _draw_column_title(draw_list, x, y, color, title, hit_rects=None, copy_text=None,
356
+ arrow_rects=None, collapsed=False):
357
+ """Draw a column's tree arrow + title. The arrow (▾ open / ▸ collapsed)
358
+ is a click target that toggles the band (registered on `arrow_rects` as
359
+ (rect, tag)); the title text is a click target that copies `copy_text`
360
+ (the whole column). Returns the title's right edge."""
361
+ # [tint=(0.95, 0.61, 0.07)]
362
+ arrow_size = 8
363
+ # [tint=(0.36, 0.68, 0.89)]
364
+ arrow_gap = 6
365
+
366
+ line_height = imgui.get_text_line_height()
367
+ cx = x + arrow_size / 2
368
+ cy = y + line_height / 2
369
+ half = arrow_size / 2
370
+ if collapsed: # ▸
371
+ draw_list.add_triangle_filled(cx - half / 2, cy - half, cx - half / 2, cy + half,
372
+ cx + half, cy, color)
373
+ else: # ▾
374
+ draw_list.add_triangle_filled(cx - half, cy - half / 2, cx + half, cy - half / 2,
375
+ cx, cy + half, color)
376
+ if arrow_rects is not None:
377
+ arrow_rects.append(((x - 2, y, x + arrow_size + arrow_gap, y + line_height), title))
378
+
379
+ text_x = x + arrow_size + arrow_gap
380
+ draw_list.add_text(text_x, y, color, title)
381
+ size = imgui.calc_text_size(title)
382
+ if hit_rects is not None and copy_text:
383
+ hit_rects.append(((text_x, y, text_x + size.x, y + size.y), copy_text, None))
384
+ return text_x + size.x
385
+
386
+
387
+ def _handle_arrow_click(arrow_rects):
388
+ """A click on a band's tree arrow toggles that band between its full
389
+ height and just its title strip. Returns True when it consumed the click."""
390
+ if not arrow_rects or not imgui.is_mouse_clicked(0):
391
+ return False
392
+ mouse = imgui.get_io().mouse_pos
393
+ for (x0, y0, x1, y1), tag in arrow_rects:
394
+ if x0 <= mouse.x <= x1 and y0 <= mouse.y <= y1:
395
+ _collapsed_tags.symmetric_difference_update({tag})
396
+ return True
397
+ return False
398
+
399
+
400
+ def _draw_scrolled_column(draw_list, io, tag, rows, column_left, content_width,
401
+ line_height, padding, viewport_top, viewport_bottom,
402
+ hit_rects, badge_rects, show_badge=True):
403
+ """Draw one category's rows inside a fixed-height scrolling viewport.
404
+
405
+ `rows` is newest-first: (label, label_color, content, content_color,
406
+ created_at, copy_text, jump). Newest sits at the viewport bottom. Scroll
407
+ is per-`tag` and STICKY at the newest end: offset 0 follows new rows as
408
+ they arrive; a scrolled-back column instead holds its place (the offset
409
+ grows with the content) and shows a clickable "N new" badge (see
410
+ _handle_badge_click) counting rows newer than the last pinned view."""
411
+ # [tint=(0.95, 0.61, 0.07)]
412
+ wheel_lines_per_tick = 3
413
+ # [tint=(0.36, 0.68, 0.89)]
414
+ thumb_width = 3
415
+ # [tint=(0.72, 0.53, 0.94)]
416
+ thumb_min_height = 24.0
417
+
418
+ viewport = (column_left - padding, viewport_top,
419
+ column_left + content_width + padding, viewport_bottom)
420
+ viewport_height = viewport_bottom - viewport_top
421
+ heights = [_entry_height(label, content, content_width, line_height, padding)
422
+ for label, _lc, content, _cc, _t, _cp, _j in rows]
423
+ content_height = sum(heights)
424
+ max_offset = max(0.0, content_height - viewport_height)
425
+
426
+ state = _scroll_state.get(tag)
427
+ if state is None:
428
+ state = _scroll_state[tag] = {"offset": 0.0, "height": 0.0, "seen_time": 0.0}
429
+
430
+ # A scrolled-back view holds still while new rows grow the column's content.
431
+ growth = content_height - state["height"]
432
+ if state["offset"] > 0 and growth > 0:
433
+ state["offset"] += growth
434
+ state["height"] = content_height
435
+
436
+ hovered = (viewport[0] <= io.mouse_pos.x <= viewport[2]
437
+ and viewport[1] <= io.mouse_pos.y <= viewport[3])
438
+ if hovered and io.mouse_wheel:
439
+ # wheel-up = back toward older rows
440
+ state["offset"] += io.mouse_wheel * line_height * wheel_lines_per_tick
441
+ state["offset"] = min(max(state["offset"], 0.0), max_offset)
442
+ offset = state["offset"]
443
+
444
+ newest_time = max((created_at for _l, _lc, _c, _cc, created_at, _cp, _j in rows),
445
+ default=0.0)
446
+ if offset <= 0:
447
+ state["seen_time"] = newest_time
448
+ unseen = (sum(1 for _l, _lc, _c, _cc, created_at, _cp, _j in rows
449
+ if created_at > state["seen_time"]) if offset > 0 else 0)
450
+
451
+ # Stack rows upward from the bottom, shifted DOWN by the scroll offset so
452
+ # older rows come into view; the clip rect swallows any overhang.
453
+ draw_list.push_clip_rect(viewport[0], viewport[1], viewport[2], viewport[3], True)
454
+ bg_bottom = viewport_bottom + offset
455
+ for (label, label_color, content, content_color, created_at,
456
+ copy_text, jump), height in zip(rows, heights):
457
+ entry_top = bg_bottom - (height - padding)
458
+ if entry_top > viewport_bottom: # newest rows scrolled out below
459
+ bg_bottom -= height
460
+ continue
461
+ if bg_bottom < viewport_top: # everything further up is out too
462
+ break
463
+ opacity = _fade_opacity(created_at)
464
+ bg_bottom = _draw_column_entry(draw_list, column_left, content_width,
465
+ line_height, padding, bg_bottom,
466
+ label, label_color, content, content_color, opacity,
467
+ hit_rects=hit_rects, copy_text=copy_text,
468
+ jump=jump, clip=viewport)
469
+ draw_list.pop_clip_rect()
470
+
471
+ # Slim scroll thumb on the column's right side while it overflows.
472
+ if max_offset > 0 and (hovered or offset > 0):
473
+ thumb_height = max(thumb_min_height,
474
+ viewport_height * viewport_height / content_height)
475
+ travel = viewport_height - thumb_height
476
+ thumb_bottom = viewport_bottom - (offset / max_offset) * travel
477
+ draw_list.add_rect_filled(viewport[2] - thumb_width, thumb_bottom - thumb_height,
478
+ viewport[2], thumb_bottom,
479
+ pack_color(1, 1, 1, 0.25), rounding=1)
480
+
481
+ if show_badge and unseen:
482
+ _draw_new_badge(draw_list, tag, unseen, column_left, content_width,
483
+ line_height, padding, viewport_bottom, badge_rects)
484
+
485
+
486
+ def _draw_new_badge(draw_list, tag, unseen, column_left, content_width,
487
+ line_height, padding, viewport_bottom, badge_rects):
488
+ """Small "N new" pill at the bottom of a scrolled-back column; clicking it
489
+ (see _handle_badge_click) jumps the column back to the sticky end."""
490
+ # [tint=(0.95, 0.61, 0.07)]
491
+ arrow_width = 6
492
+ # [tint=(0.36, 0.68, 0.89)]
493
+ pill_extra_width = 6
494
+
495
+ text = f"{unseen} new"
496
+ text_size = imgui.calc_text_size(text)
497
+ pill_width = text_size.x + arrow_width + padding * 4 + pill_extra_width
498
+ pill_height = line_height + padding * 2
499
+ x0 = column_left + (content_width - pill_width) / 2
500
+ y1 = viewport_bottom - padding * 2
501
+ y0 = y1 - pill_height
502
+ yellow = pack_color(1, 1, 0, 1)
503
+ draw_list.add_rect_filled(x0, y0, x0 + pill_width, y1,
504
+ pack_color(0, 0, 0, 0.9),
505
+ rounding=pill_height / 2)
506
+ draw_list.add_rect(x0, y0, x0 + pill_width, y1, yellow,
507
+ rounding=pill_height / 2, thickness=1.0)
508
+ text_x = x0 + padding * 2 + 2
509
+ draw_list.add_text(text_x, y0 + padding, yellow, text)
510
+ # down-pointing triangle after the text ("new toasts are below")
511
+ arrow_x = text_x + text_size.x + 4
512
+ arrow_y = y0 + pill_height / 2
513
+ draw_list.add_triangle_filled(arrow_x, arrow_y - 3, arrow_x + arrow_width, arrow_y - 3,
514
+ arrow_x + arrow_width / 2, arrow_y + 3, yellow)
515
+ badge_rects.append(((x0, y0, x0 + pill_width, y1), tag))
516
+
517
+
518
+ # Per-tag scroll state, keyed by column title. "offset" is pixels scrolled
519
+ # back from the newest end - 0 means pinned to the end, so new toasts
520
+ # auto-scroll into view (sticky). "height" is last frame's content height,
521
+ # used to hold a scrolled-back view still while new toasts grow the column.
522
+ # "seen_time" is the newest created_at that was on screen while pinned;
523
+ # anything newer feeds the "N new" badge. Hotswap-safe.
524
+ _scroll_state = globals().get("_scroll_state") or {}
525
+
526
+
527
+ def _file_tint(path):
528
+ """The tint the user painted on `path` in the code editor (FileMeta), as
529
+ RGBA, or None. Same store the editor tabs / folder tree read."""
530
+ try:
531
+ from meltygui.editor.source_ui import _file_meta_tint
532
+ rgb = _file_meta_tint(path)
533
+ except Exception:
534
+ return None
535
+ return (*rgb, 1) if rgb else None
536
+
537
+
538
+ # Rect of the most recently copied entry and when it was copied, so the click gets
539
+ # a brief visual confirmation. Guarded so a hotswap re-exec keeps the value.
540
+ _copy_flash = globals().get("_copy_flash")
541
+ _COPY_FLASH_SECONDS = 0.6
542
+
543
+
544
+ def _handle_badge_click(badge_rects):
545
+ """A click on a column's "N new" badge jumps that column back to the
546
+ sticky end (offset 0), where the seen-marker resets on the next frame.
547
+ Returns True when a badge consumed the click."""
548
+ if not badge_rects or not imgui.is_mouse_clicked(0):
549
+ return False
550
+ mouse = imgui.get_io().mouse_pos
551
+ for (x0, y0, x1, y1), tag in badge_rects:
552
+ if x0 <= mouse.x <= x1 and y0 <= mouse.y <= y1:
553
+ state = _scroll_state.get(tag)
554
+ if state is not None:
555
+ state["offset"] = 0.0
556
+ return True
557
+ return False
558
+
559
+
560
+ def _handle_entry_click(hit_rects):
561
+ """Click handling: a click inside an entry with a `jump` opens that
562
+ (path, line) in the code editor (global search's jump path); otherwise —
563
+ and always with ctrl held — the entry's text goes on the clipboard. Either
564
+ way the entry flashes."""
565
+ global _copy_flash
566
+ if not hit_rects or not imgui.is_mouse_clicked(0):
567
+ return
568
+ io = imgui.get_io()
569
+ mx, my = io.mouse_pos.x, io.mouse_pos.y
570
+ # Later entries are drawn above earlier ones and never overlap, so the first
571
+ # containing rect is the hit.
572
+ for (x0, y0, x1, y1), text, jump in hit_rects:
573
+ if x0 <= mx <= x1 and y0 <= my <= y1:
574
+ if jump is not None and not io.key_ctrl:
575
+ from meltygui.core.runtime.extensions import open_source as open_in_editor
576
+ open_in_editor(jump[0], jump[1])
577
+ else:
578
+ imgui.set_clipboard_text(text)
579
+ _copy_flash = ((x0, y0, x1, y1), time.time())
580
+ return
581
+
582
+
583
+ def _draw_copy_flash(draw_list):
584
+ """Outline the entry that was just copied, fading out over ~0.6s."""
585
+ if _copy_flash is None:
586
+ return
587
+ (x0, y0, x1, y1), copied_at = _copy_flash
588
+ remaining = _COPY_FLASH_SECONDS - (time.time() - copied_at)
589
+ if remaining <= 0:
590
+ return
591
+ alpha = remaining / _COPY_FLASH_SECONDS
592
+ draw_list.add_rect(x0, y0, x1, y1,
593
+ pack_color(1, 1, 0, alpha), rounding=2, thickness=1.5)
594
+ from meltygui.core.windowing.glfw_utils import request_render
595
+ request_render()
596
+
597
+
598
+ def draw_notifications():
599
+ io = imgui.get_io()
600
+ display_size = io.display_size
601
+ draw_list = imgui.get_overlay_draw_list()
602
+
603
+ from meltygui.core.melty import Melty
604
+ font_handle = Melty.font_mgr.get(Font.JETBRAINS_MONO_14) if Melty.font_mgr else None
605
+ if font_handle is not None:
606
+ imgui.push_font(font_handle)
607
+
608
+ try:
609
+ # [tint=(0.36, 0.68, 0.89)]
610
+ padding = 2
611
+ # [tint=(0.95, 0.61, 0.07)]
612
+ column_width = 300 # also the toast max width
613
+ # Bottom strip of each band that its title text sits in.
614
+ # [tint=(0.72, 0.53, 0.94)]
615
+ title_strip = 30
616
+ # Gap between the bands and the window's right edge.
617
+ # [tint=(0.42, 0.79, 0.42)]
618
+ edge_margin = 10
619
+ # Per-category band height - a Toggles knob, applied live.
620
+ category_height = Toggles.Notifications.category_height
621
+
622
+ content_width = column_width - padding * 2 # left-aligned content box
623
+ line_height = imgui.get_text_line_height()
624
+ title_color = pack_color(1, 1, 0, 1)
625
+
626
+ tagged_columns = [(tag, notifications) for tag, notifications
627
+ in NotificationCenter.tagged_notifications.items()
628
+ if tag not in NotificationCenter.ignore_tags]
629
+ live_entries = [(tag + " ", value_str)
630
+ for tag, (value_str, _c, _t, _a) in NotificationCenter.live_values.items()]
631
+
632
+ n_columns = len(tagged_columns) + (1 if live_entries else 0)
633
+ if n_columns == 0:
634
+ return
635
+
636
+ # Categories stack VERTICALLY along the right edge: the first tag's
637
+ # history in the bottom-right corner, each further category in its own
638
+ # fixed-height band above it, "Live" topmost. Within each band the title
639
+ # is at the bottom with the scrolling viewport above it.
640
+ column_left = display_size.x - column_width - edge_margin
641
+ hit_rects = []
642
+ badge_rects = []
643
+ arrow_rects = []
644
+ # Bottom of the NEXT band to lay out; bands stack upward, a collapsed
645
+ # band taking only its title strip so the ones above it pack down.
646
+ next_band_bottom = [display_size.y]
647
+
648
+ def band_rect(tag):
649
+ """(title_top, viewport_top, viewport_bottom, collapsed) of `tag`'s
650
+ band, stacked above the previously laid-out one."""
651
+ collapsed = tag in _collapsed_tags
652
+ band_bottom = next_band_bottom[0]
653
+ title_top = band_bottom - title_strip
654
+ height = title_strip if collapsed else category_height
655
+ next_band_bottom[0] = band_bottom - height
656
+ return title_top, band_bottom - height, title_top - padding, collapsed
657
+
658
+ for tag, notifications in tagged_columns:
659
+ title_top, viewport_top, viewport_bottom, collapsed = band_rect(tag)
660
+
661
+ # tag title pinned at the bottom of the band; clicking it copies
662
+ # the band's whole history, not just the entries on screen.
663
+ _draw_column_title(draw_list, column_left, title_top, title_color, tag,
664
+ hit_rects, "\n".join(n.copy_text for n in notifications),
665
+ arrow_rects=arrow_rects, collapsed=collapsed)
666
+ if collapsed:
667
+ continue
668
+
669
+ rows = []
670
+ for n in notifications:
671
+ # Entries that jump to a file take that file's editor tint.
672
+ tint = (_file_tint(n.jump[0]) if n.jump is not None else None) or n.tint
673
+ rows.append((n.time_label, tint, n.label, tint, n.created_at,
674
+ n.copy_text, n.jump))
675
+ _draw_scrolled_column(draw_list, io, tag, rows, column_left, content_width,
676
+ line_height, padding, viewport_top, viewport_bottom,
677
+ hit_rects, badge_rects)
678
+
679
+ # dedicated "Live" band above the tagged notification bands - each row
680
+ # is one tag's current value, tinted, updated in place over time - so
681
+ # it scrolls like the others but skips the "new" badge (in-place
682
+ # updates would keep it lit permanently).
683
+ if live_entries:
684
+ title_top, viewport_top, viewport_bottom, collapsed = band_rect("Live")
685
+ label_color = (0.6, 0.6, 0.6, 1)
686
+
687
+ _draw_column_title(draw_list, column_left, title_top, title_color, "Live",
688
+ hit_rects, "\n".join(f"{label}{value_str}"
689
+ for label, value_str in live_entries),
690
+ arrow_rects=arrow_rects, collapsed=collapsed)
691
+
692
+ if live_entries and not collapsed:
693
+ rows = [(live_tag + " ", label_color, value_str, color, created_at, None, None)
694
+ for live_tag, (value_str, color, _time_label, created_at)
695
+ in NotificationCenter.live_values.items()]
696
+ _draw_scrolled_column(draw_list, io, "Live", rows, column_left, content_width,
697
+ line_height, padding, viewport_top, viewport_bottom,
698
+ hit_rects, badge_rects, show_badge=False)
699
+
700
+ if not _handle_arrow_click(arrow_rects) and not _handle_badge_click(badge_rects):
701
+ _handle_entry_click(hit_rects)
702
+ _draw_copy_flash(draw_list)
703
+ finally:
704
+ if font_handle is not None:
705
+ imgui.pop_font()
706
+