bootstack 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 (471) hide show
  1. bootstack/__init__.py +157 -0
  2. bootstack/__main__.py +5 -0
  3. bootstack/_core/__init__.py +21 -0
  4. bootstack/_core/capabilities/__init__.py +45 -0
  5. bootstack/_core/capabilities/after.py +103 -0
  6. bootstack/_core/capabilities/bind.py +192 -0
  7. bootstack/_core/capabilities/bindtags.py +112 -0
  8. bootstack/_core/capabilities/busy.py +72 -0
  9. bootstack/_core/capabilities/clipboard.py +89 -0
  10. bootstack/_core/capabilities/focus.py +118 -0
  11. bootstack/_core/capabilities/grab.py +65 -0
  12. bootstack/_core/capabilities/grid.py +211 -0
  13. bootstack/_core/capabilities/localization.py +231 -0
  14. bootstack/_core/capabilities/pack.py +140 -0
  15. bootstack/_core/capabilities/place.py +113 -0
  16. bootstack/_core/capabilities/selection.py +136 -0
  17. bootstack/_core/capabilities/signals.py +244 -0
  18. bootstack/_core/capabilities/winfo.py +315 -0
  19. bootstack/_core/colorutils.py +234 -0
  20. bootstack/_core/exceptions.py +25 -0
  21. bootstack/_core/images.py +463 -0
  22. bootstack/_core/mixins/__init__.py +1 -0
  23. bootstack/_core/mixins/ttk_state.py +35 -0
  24. bootstack/_core/mixins/widget.py +132 -0
  25. bootstack/_core/paths.py +49 -0
  26. bootstack/_core/publisher.py +149 -0
  27. bootstack/_core/variables.py +62 -0
  28. bootstack/_runtime/__init__.py +3 -0
  29. bootstack/_runtime/app.py +930 -0
  30. bootstack/_runtime/base_window.py +945 -0
  31. bootstack/_runtime/events.py +399 -0
  32. bootstack/_runtime/shortcuts.py +496 -0
  33. bootstack/_runtime/tk_patch.py +43 -0
  34. bootstack/_runtime/toplevel.py +276 -0
  35. bootstack/_runtime/utility.py +457 -0
  36. bootstack/_runtime/visual_focus.py +240 -0
  37. bootstack/_runtime/window_utilities.py +1123 -0
  38. bootstack/assets/__init__.py +21 -0
  39. bootstack/assets/bootstack.ico +0 -0
  40. bootstack/assets/bootstack.png +0 -0
  41. bootstack/assets/elements/__init__.py +0 -0
  42. bootstack/assets/elements/badge-pill.png +0 -0
  43. bootstack/assets/elements/badge-square.png +0 -0
  44. bootstack/assets/elements/button-compact.png +0 -0
  45. bootstack/assets/elements/button-default.png +0 -0
  46. bootstack/assets/elements/buttongroup-after-h-compact.png +0 -0
  47. bootstack/assets/elements/buttongroup-after-h-default.png +0 -0
  48. bootstack/assets/elements/buttongroup-after-v-compact.png +0 -0
  49. bootstack/assets/elements/buttongroup-after-v-default.png +0 -0
  50. bootstack/assets/elements/buttongroup-before-h-compact.png +0 -0
  51. bootstack/assets/elements/buttongroup-before-h-default.png +0 -0
  52. bootstack/assets/elements/buttongroup-before-v-compact.png +0 -0
  53. bootstack/assets/elements/buttongroup-before-v-default.png +0 -0
  54. bootstack/assets/elements/buttongroup-center-h-compact.png +0 -0
  55. bootstack/assets/elements/buttongroup-center-h-default.png +0 -0
  56. bootstack/assets/elements/buttongroup-center-v-compact.png +0 -0
  57. bootstack/assets/elements/buttongroup-center-v-default.png +0 -0
  58. bootstack/assets/elements/card.png +0 -0
  59. bootstack/assets/elements/checkbox-checked.png +0 -0
  60. bootstack/assets/elements/checkbox-indeterminate.png +0 -0
  61. bootstack/assets/elements/checkbox-unchecked.png +0 -0
  62. bootstack/assets/elements/field.png +0 -0
  63. bootstack/assets/elements/input-addon-compact.png +0 -0
  64. bootstack/assets/elements/input-addon-default.png +0 -0
  65. bootstack/assets/elements/input-compact.png +0 -0
  66. bootstack/assets/elements/input-default.png +0 -0
  67. bootstack/assets/elements/list-item-separated.png +0 -0
  68. bootstack/assets/elements/list-item.png +0 -0
  69. bootstack/assets/elements/listrow-compact.png +0 -0
  70. bootstack/assets/elements/listrow-default.png +0 -0
  71. bootstack/assets/elements/manifest.toml +361 -0
  72. bootstack/assets/elements/menu-item.png +0 -0
  73. bootstack/assets/elements/navitem-compact.png +0 -0
  74. bootstack/assets/elements/navitem-default.png +0 -0
  75. bootstack/assets/elements/progressbar-h-compact.png +0 -0
  76. bootstack/assets/elements/progressbar-h-default.png +0 -0
  77. bootstack/assets/elements/progressbar-v-compact.png +0 -0
  78. bootstack/assets/elements/progressbar-v-default.png +0 -0
  79. bootstack/assets/elements/radiobutton.png +0 -0
  80. bootstack/assets/elements/scrollbar-horizontal.png +0 -0
  81. bootstack/assets/elements/scrollbar-vertical.png +0 -0
  82. bootstack/assets/elements/slider-handle.png +0 -0
  83. bootstack/assets/elements/slider-track-h.png +0 -0
  84. bootstack/assets/elements/slider-track-v.png +0 -0
  85. bootstack/assets/elements/switch-off.png +0 -0
  86. bootstack/assets/elements/switch-on.png +0 -0
  87. bootstack/assets/elements/tab-h.png +0 -0
  88. bootstack/assets/elements/tab-v.png +0 -0
  89. bootstack/assets/icons/bootstrap.ttf +0 -0
  90. bootstack/assets/icons/glyphmap.json +2080 -0
  91. bootstack/assets/icons/icon_metrics.json +12470 -0
  92. bootstack/assets/locales/ar/LC_MESSAGES/bootstack.mo +0 -0
  93. bootstack/assets/locales/ar/LC_MESSAGES/bootstack.po +856 -0
  94. bootstack/assets/locales/bg/LC_MESSAGES/bootstack.mo +0 -0
  95. bootstack/assets/locales/bg/LC_MESSAGES/bootstack.po +878 -0
  96. bootstack/assets/locales/cs/LC_MESSAGES/bootstack.mo +0 -0
  97. bootstack/assets/locales/cs/LC_MESSAGES/bootstack.po +856 -0
  98. bootstack/assets/locales/da/LC_MESSAGES/bootstack.mo +0 -0
  99. bootstack/assets/locales/da/LC_MESSAGES/bootstack.po +856 -0
  100. bootstack/assets/locales/de/LC_MESSAGES/bootstack.mo +0 -0
  101. bootstack/assets/locales/de/LC_MESSAGES/bootstack.po +856 -0
  102. bootstack/assets/locales/en/LC_MESSAGES/bootstack.mo +0 -0
  103. bootstack/assets/locales/en/LC_MESSAGES/bootstack.po +878 -0
  104. bootstack/assets/locales/es/LC_MESSAGES/bootstack.mo +0 -0
  105. bootstack/assets/locales/es/LC_MESSAGES/bootstack.po +856 -0
  106. bootstack/assets/locales/fr/LC_MESSAGES/bootstack.mo +0 -0
  107. bootstack/assets/locales/fr/LC_MESSAGES/bootstack.po +856 -0
  108. bootstack/assets/locales/he/LC_MESSAGES/bootstack.mo +0 -0
  109. bootstack/assets/locales/he/LC_MESSAGES/bootstack.po +854 -0
  110. bootstack/assets/locales/hi/LC_MESSAGES/bootstack.mo +0 -0
  111. bootstack/assets/locales/hi/LC_MESSAGES/bootstack.po +845 -0
  112. bootstack/assets/locales/it/LC_MESSAGES/bootstack.mo +0 -0
  113. bootstack/assets/locales/it/LC_MESSAGES/bootstack.po +844 -0
  114. bootstack/assets/locales/ja/LC_MESSAGES/bootstack.mo +0 -0
  115. bootstack/assets/locales/ja/LC_MESSAGES/bootstack.po +917 -0
  116. bootstack/assets/locales/ko/LC_MESSAGES/bootstack.mo +0 -0
  117. bootstack/assets/locales/ko/LC_MESSAGES/bootstack.po +845 -0
  118. bootstack/assets/locales/nb/LC_MESSAGES/bootstack.mo +0 -0
  119. bootstack/assets/locales/nb/LC_MESSAGES/bootstack.po +844 -0
  120. bootstack/assets/locales/nl/LC_MESSAGES/bootstack.mo +0 -0
  121. bootstack/assets/locales/nl/LC_MESSAGES/bootstack.po +844 -0
  122. bootstack/assets/locales/pl/LC_MESSAGES/bootstack.mo +0 -0
  123. bootstack/assets/locales/pl/LC_MESSAGES/bootstack.po +845 -0
  124. bootstack/assets/locales/pt/LC_MESSAGES/bootstack.mo +0 -0
  125. bootstack/assets/locales/pt/LC_MESSAGES/bootstack.po +845 -0
  126. bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.mo +0 -0
  127. bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.po +845 -0
  128. bootstack/assets/locales/sl/LC_MESSAGES/bootstack.mo +0 -0
  129. bootstack/assets/locales/sl/LC_MESSAGES/bootstack.po +845 -0
  130. bootstack/assets/locales/sv/LC_MESSAGES/bootstack.mo +0 -0
  131. bootstack/assets/locales/sv/LC_MESSAGES/bootstack.po +845 -0
  132. bootstack/assets/locales/tr/LC_MESSAGES/bootstack.mo +0 -0
  133. bootstack/assets/locales/tr/LC_MESSAGES/bootstack.po +845 -0
  134. bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.mo +0 -0
  135. bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.po +845 -0
  136. bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.mo +0 -0
  137. bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.po +845 -0
  138. bootstack/cli/__init__.py +133 -0
  139. bootstack/cli/__main__.py +6 -0
  140. bootstack/cli/add.py +395 -0
  141. bootstack/cli/appicon.py +285 -0
  142. bootstack/cli/build.py +115 -0
  143. bootstack/cli/config.py +313 -0
  144. bootstack/cli/demo.py +564 -0
  145. bootstack/cli/dev.py +153 -0
  146. bootstack/cli/doctor.py +195 -0
  147. bootstack/cli/icons.py +98 -0
  148. bootstack/cli/promote.py +120 -0
  149. bootstack/cli/pyinstaller.py +268 -0
  150. bootstack/cli/run.py +95 -0
  151. bootstack/cli/start.py +117 -0
  152. bootstack/cli/templates/__init__.py +931 -0
  153. bootstack/clipboard.py +48 -0
  154. bootstack/constants.py +318 -0
  155. bootstack/data/README.md +615 -0
  156. bootstack/data/__init__.py +78 -0
  157. bootstack/data/_observable.py +276 -0
  158. bootstack/data/base.py +780 -0
  159. bootstack/data/file_source.py +367 -0
  160. bootstack/data/memory_source.py +450 -0
  161. bootstack/data/query.py +367 -0
  162. bootstack/data/readers.py +289 -0
  163. bootstack/data/sqlite_source.py +869 -0
  164. bootstack/data/types.py +354 -0
  165. bootstack/data/writers.py +232 -0
  166. bootstack/dev/__init__.py +36 -0
  167. bootstack/dev/_capture.py +141 -0
  168. bootstack/dev/_env.py +43 -0
  169. bootstack/dev/_registry.py +140 -0
  170. bootstack/dev/_reloader.py +351 -0
  171. bootstack/dev/_reset.py +50 -0
  172. bootstack/dev/_watcher.py +91 -0
  173. bootstack/dialogs/__init__.py +948 -0
  174. bootstack/dialogs/_impl/__init__.py +47 -0
  175. bootstack/dialogs/_impl/colorchooser.py +588 -0
  176. bootstack/dialogs/_impl/datedialog.py +450 -0
  177. bootstack/dialogs/_impl/dialog.py +594 -0
  178. bootstack/dialogs/_impl/filterdialog.py +358 -0
  179. bootstack/dialogs/_impl/fontdialog.py +364 -0
  180. bootstack/dialogs/_impl/formdialog.py +564 -0
  181. bootstack/dialogs/_impl/message.py +486 -0
  182. bootstack/dialogs/_impl/query.py +570 -0
  183. bootstack/errors.py +67 -0
  184. bootstack/events/__init__.py +111 -0
  185. bootstack/events/_event.py +135 -0
  186. bootstack/events/_payloads.py +539 -0
  187. bootstack/events/_subscription.py +38 -0
  188. bootstack/i18n/README.md +77 -0
  189. bootstack/i18n/__init__.py +23 -0
  190. bootstack/i18n/catalog.py +121 -0
  191. bootstack/i18n/intl_format.py +584 -0
  192. bootstack/i18n/msgcat.py +425 -0
  193. bootstack/i18n/specs.py +156 -0
  194. bootstack/images.py +563 -0
  195. bootstack/py.typed +1 -0
  196. bootstack/scheduling/__init__.py +11 -0
  197. bootstack/scheduling/_schedule.py +218 -0
  198. bootstack/shortcuts.py +21 -0
  199. bootstack/signals/README.md +98 -0
  200. bootstack/signals/__init__.py +10 -0
  201. bootstack/signals/integration.py +100 -0
  202. bootstack/signals/signal.py +353 -0
  203. bootstack/signals/types.py +6 -0
  204. bootstack/store.py +286 -0
  205. bootstack/streams/__init__.py +12 -0
  206. bootstack/streams/_stream.py +321 -0
  207. bootstack/style/__init__.py +38 -0
  208. bootstack/style/builders/__init__.py +51 -0
  209. bootstack/style/builders/badge.py +46 -0
  210. bootstack/style/builders/button.py +339 -0
  211. bootstack/style/builders/buttongroup.py +311 -0
  212. bootstack/style/builders/calendar.py +271 -0
  213. bootstack/style/builders/checkbutton.py +110 -0
  214. bootstack/style/builders/combobox.py +113 -0
  215. bootstack/style/builders/contextmenu.py +268 -0
  216. bootstack/style/builders/entry.py +82 -0
  217. bootstack/style/builders/expander.py +148 -0
  218. bootstack/style/builders/field.py +335 -0
  219. bootstack/style/builders/frame.py +50 -0
  220. bootstack/style/builders/label.py +28 -0
  221. bootstack/style/builders/labelframe.py +34 -0
  222. bootstack/style/builders/listview.py +369 -0
  223. bootstack/style/builders/menubar.py +91 -0
  224. bootstack/style/builders/menubutton.py +359 -0
  225. bootstack/style/builders/panedwindow.py +25 -0
  226. bootstack/style/builders/progressbar.py +67 -0
  227. bootstack/style/builders/radiobutton.py +99 -0
  228. bootstack/style/builders/scale.py +64 -0
  229. bootstack/style/builders/scrollbar.py +225 -0
  230. bootstack/style/builders/separator.py +49 -0
  231. bootstack/style/builders/sidenav.py +643 -0
  232. bootstack/style/builders/sizegrip.py +15 -0
  233. bootstack/style/builders/spinbox.py +119 -0
  234. bootstack/style/builders/switch.py +70 -0
  235. bootstack/style/builders/tabitem.py +204 -0
  236. bootstack/style/builders/togglegroup.py +294 -0
  237. bootstack/style/builders/toolbutton.py +275 -0
  238. bootstack/style/builders/tooltip.py +26 -0
  239. bootstack/style/builders/treeview.py +193 -0
  240. bootstack/style/builders/utils.py +455 -0
  241. bootstack/style/builders_tk/__init__.py +16 -0
  242. bootstack/style/builders_tk/defaults.py +229 -0
  243. bootstack/style/element.py +173 -0
  244. bootstack/style/fonts.py +123 -0
  245. bootstack/style/style.py +609 -0
  246. bootstack/style/style_builder_base.py +716 -0
  247. bootstack/style/style_builder_mixed.py +93 -0
  248. bootstack/style/style_builder_tk.py +109 -0
  249. bootstack/style/style_builder_ttk.py +353 -0
  250. bootstack/style/style_resolver.py +447 -0
  251. bootstack/style/theme.py +245 -0
  252. bootstack/style/theme_provider.py +471 -0
  253. bootstack/style/themes/__init__.py +128 -0
  254. bootstack/style/tk_patch.py +5 -0
  255. bootstack/style/token_maps.py +41 -0
  256. bootstack/style/types.py +32 -0
  257. bootstack/style/typography.py +523 -0
  258. bootstack/style/utility.py +746 -0
  259. bootstack/types.py +39 -0
  260. bootstack/validation/__init__.py +6 -0
  261. bootstack/validation/types.py +15 -0
  262. bootstack/validation/validation_result.py +17 -0
  263. bootstack/validation/validation_rules.py +205 -0
  264. bootstack/widgets/__init__.py +74 -0
  265. bootstack/widgets/_core/__init__.py +31 -0
  266. bootstack/widgets/_core/app_config.py +266 -0
  267. bootstack/widgets/_core/base.py +461 -0
  268. bootstack/widgets/_core/container.py +334 -0
  269. bootstack/widgets/_core/context.py +35 -0
  270. bootstack/widgets/_core/events.py +130 -0
  271. bootstack/widgets/_core/field_mixin.py +353 -0
  272. bootstack/widgets/_core/icon_image_props.py +72 -0
  273. bootstack/widgets/_core/image_binding.py +93 -0
  274. bootstack/widgets/_core/navmodel.py +345 -0
  275. bootstack/widgets/_core/options.py +210 -0
  276. bootstack/widgets/_core/selection_group.py +130 -0
  277. bootstack/widgets/_core/window_controls.py +96 -0
  278. bootstack/widgets/_core/window_menu.py +246 -0
  279. bootstack/widgets/_impl/__init__.py +1 -0
  280. bootstack/widgets/_impl/_internal/__init__.py +0 -0
  281. bootstack/widgets/_impl/_internal/wrapper_base.py +307 -0
  282. bootstack/widgets/_impl/_parts/__init__.py +11 -0
  283. bootstack/widgets/_impl/_parts/numberentry_part.py +385 -0
  284. bootstack/widgets/_impl/_parts/spinnerentry_part.py +434 -0
  285. bootstack/widgets/_impl/_parts/textentry_part.py +406 -0
  286. bootstack/widgets/_impl/composites/__init__.py +33 -0
  287. bootstack/widgets/_impl/composites/_dateutils.py +33 -0
  288. bootstack/widgets/_impl/composites/_image_fit.py +105 -0
  289. bootstack/widgets/_impl/composites/accordion.py +381 -0
  290. bootstack/widgets/_impl/composites/avatar.py +191 -0
  291. bootstack/widgets/_impl/composites/buttongroup.py +371 -0
  292. bootstack/widgets/_impl/composites/calendar.py +972 -0
  293. bootstack/widgets/_impl/composites/carousel.py +567 -0
  294. bootstack/widgets/_impl/composites/chart.py +882 -0
  295. bootstack/widgets/_impl/composites/compositeframe.py +298 -0
  296. bootstack/widgets/_impl/composites/contextmenu.py +1951 -0
  297. bootstack/widgets/_impl/composites/dateentry.py +404 -0
  298. bootstack/widgets/_impl/composites/dropdownbutton.py +325 -0
  299. bootstack/widgets/_impl/composites/expander.py +515 -0
  300. bootstack/widgets/_impl/composites/field.py +670 -0
  301. bootstack/widgets/_impl/composites/form.py +1066 -0
  302. bootstack/widgets/_impl/composites/gallery.py +551 -0
  303. bootstack/widgets/_impl/composites/list/__init__.py +15 -0
  304. bootstack/widgets/_impl/composites/list/listitem.py +802 -0
  305. bootstack/widgets/_impl/composites/list/listview.py +1433 -0
  306. bootstack/widgets/_impl/composites/menu/__init__.py +18 -0
  307. bootstack/widgets/_impl/composites/menu/model.py +358 -0
  308. bootstack/widgets/_impl/composites/menu/render_native.py +134 -0
  309. bootstack/widgets/_impl/composites/menu/render_themed.py +134 -0
  310. bootstack/widgets/_impl/composites/meter.py +860 -0
  311. bootstack/widgets/_impl/composites/numericentry.py +201 -0
  312. bootstack/widgets/_impl/composites/pagestack.py +395 -0
  313. bootstack/widgets/_impl/composites/passwordentry.py +142 -0
  314. bootstack/widgets/_impl/composites/pathentry.py +168 -0
  315. bootstack/widgets/_impl/composites/picture.py +289 -0
  316. bootstack/widgets/_impl/composites/radiogroup.py +511 -0
  317. bootstack/widgets/_impl/composites/scrolledtext.py +375 -0
  318. bootstack/widgets/_impl/composites/scrolledtext.pyi +186 -0
  319. bootstack/widgets/_impl/composites/scrollview.py +764 -0
  320. bootstack/widgets/_impl/composites/selectbox.py +1026 -0
  321. bootstack/widgets/_impl/composites/shell/__init__.py +40 -0
  322. bootstack/widgets/_impl/composites/shell/content_host.py +52 -0
  323. bootstack/widgets/_impl/composites/shell/layout.py +335 -0
  324. bootstack/widgets/_impl/composites/shell/nav_panel.py +345 -0
  325. bootstack/widgets/_impl/composites/shell/providers.py +558 -0
  326. bootstack/widgets/_impl/composites/shell/rail.py +117 -0
  327. bootstack/widgets/_impl/composites/shell/shell.py +581 -0
  328. bootstack/widgets/_impl/composites/shell/workspace.py +273 -0
  329. bootstack/widgets/_impl/composites/sidenav/__init__.py +16 -0
  330. bootstack/widgets/_impl/composites/sidenav/header.py +81 -0
  331. bootstack/widgets/_impl/composites/sidenav/separator.py +44 -0
  332. bootstack/widgets/_impl/composites/slider/__init__.py +7 -0
  333. bootstack/widgets/_impl/composites/slider/_shared.py +195 -0
  334. bootstack/widgets/_impl/composites/slider/rangeslider.py +982 -0
  335. bootstack/widgets/_impl/composites/slider/slider.py +851 -0
  336. bootstack/widgets/_impl/composites/spinnerentry.py +185 -0
  337. bootstack/widgets/_impl/composites/tableview/__init__.py +5 -0
  338. bootstack/widgets/_impl/composites/tableview/tableview.py +3388 -0
  339. bootstack/widgets/_impl/composites/tableview/types.py +169 -0
  340. bootstack/widgets/_impl/composites/tabs/__init__.py +23 -0
  341. bootstack/widgets/_impl/composites/tabs/tabitem.py +389 -0
  342. bootstack/widgets/_impl/composites/tabs/tabs.py +974 -0
  343. bootstack/widgets/_impl/composites/tabs/tabview.py +650 -0
  344. bootstack/widgets/_impl/composites/textarea/__init__.py +26 -0
  345. bootstack/widgets/_impl/composites/textarea/change.py +46 -0
  346. bootstack/widgets/_impl/composites/textarea/codeeditor.py +526 -0
  347. bootstack/widgets/_impl/composites/textarea/core.py +495 -0
  348. bootstack/widgets/_impl/composites/textarea/decoration.py +42 -0
  349. bootstack/widgets/_impl/composites/textarea/diff.py +127 -0
  350. bootstack/widgets/_impl/composites/textarea/extensions/__init__.py +1 -0
  351. bootstack/widgets/_impl/composites/textarea/extensions/bracket_matcher.py +138 -0
  352. bootstack/widgets/_impl/composites/textarea/extensions/indent_guides.py +158 -0
  353. bootstack/widgets/_impl/composites/textarea/extensions/line_numbers.py +138 -0
  354. bootstack/widgets/_impl/composites/textarea/extensions/pygments_highlighter.py +312 -0
  355. bootstack/widgets/_impl/composites/textarea/extensions/smart_indent.py +177 -0
  356. bootstack/widgets/_impl/composites/textarea/filter.py +171 -0
  357. bootstack/widgets/_impl/composites/textarea/search_overlay.py +459 -0
  358. bootstack/widgets/_impl/composites/textarea/sidebar.py +88 -0
  359. bootstack/widgets/_impl/composites/textarea/style_registry.py +178 -0
  360. bootstack/widgets/_impl/composites/textarea/textarea.py +606 -0
  361. bootstack/widgets/_impl/composites/textarea/undo.py +217 -0
  362. bootstack/widgets/_impl/composites/textentry.py +57 -0
  363. bootstack/widgets/_impl/composites/timeentry.py +176 -0
  364. bootstack/widgets/_impl/composites/toast.py +390 -0
  365. bootstack/widgets/_impl/composites/toast_stack.py +156 -0
  366. bootstack/widgets/_impl/composites/togglegroup.py +418 -0
  367. bootstack/widgets/_impl/composites/toolbar.py +608 -0
  368. bootstack/widgets/_impl/composites/tooltip.py +491 -0
  369. bootstack/widgets/_impl/composites/tree/__init__.py +7 -0
  370. bootstack/widgets/_impl/composites/tree/source_binding.py +136 -0
  371. bootstack/widgets/_impl/composites/tree/treeitem.py +393 -0
  372. bootstack/widgets/_impl/composites/tree/treenode.py +174 -0
  373. bootstack/widgets/_impl/composites/tree/treeview.py +841 -0
  374. bootstack/widgets/_impl/mixins/__init__.py +24 -0
  375. bootstack/widgets/_impl/mixins/configure_mixin.py +216 -0
  376. bootstack/widgets/_impl/mixins/entry_mixin.py +134 -0
  377. bootstack/widgets/_impl/mixins/font_mixin.py +368 -0
  378. bootstack/widgets/_impl/mixins/icon_mixin.py +61 -0
  379. bootstack/widgets/_impl/mixins/localization_mixin.py +253 -0
  380. bootstack/widgets/_impl/mixins/signal_mixin.py +268 -0
  381. bootstack/widgets/_impl/mixins/validation_mixin.py +226 -0
  382. bootstack/widgets/_impl/primitives/__init__.py +49 -0
  383. bootstack/widgets/_impl/primitives/_menubutton.py +107 -0
  384. bootstack/widgets/_impl/primitives/badge.py +45 -0
  385. bootstack/widgets/_impl/primitives/button.py +76 -0
  386. bootstack/widgets/_impl/primitives/card.py +45 -0
  387. bootstack/widgets/_impl/primitives/checkbutton.py +124 -0
  388. bootstack/widgets/_impl/primitives/checktoggle.py +62 -0
  389. bootstack/widgets/_impl/primitives/combobox.py +156 -0
  390. bootstack/widgets/_impl/primitives/entry.py +87 -0
  391. bootstack/widgets/_impl/primitives/flexframe.py +448 -0
  392. bootstack/widgets/_impl/primitives/frame.py +185 -0
  393. bootstack/widgets/_impl/primitives/gridframe.py +546 -0
  394. bootstack/widgets/_impl/primitives/label.py +84 -0
  395. bootstack/widgets/_impl/primitives/labelframe.py +54 -0
  396. bootstack/widgets/_impl/primitives/optionmenu.py +387 -0
  397. bootstack/widgets/_impl/primitives/packframe.py +227 -0
  398. bootstack/widgets/_impl/primitives/panedwindow.py +45 -0
  399. bootstack/widgets/_impl/primitives/progressbar.py +83 -0
  400. bootstack/widgets/_impl/primitives/radiobutton.py +115 -0
  401. bootstack/widgets/_impl/primitives/radiotoggle.py +54 -0
  402. bootstack/widgets/_impl/primitives/scrollbar.py +42 -0
  403. bootstack/widgets/_impl/primitives/separator.py +43 -0
  404. bootstack/widgets/_impl/primitives/sizegrip.py +33 -0
  405. bootstack/widgets/_impl/primitives/spinbox.py +95 -0
  406. bootstack/widgets/_impl/primitives/switch.py +44 -0
  407. bootstack/widgets/_impl/primitives/treeview.py +69 -0
  408. bootstack/widgets/app.py +371 -0
  409. bootstack/widgets/appshell.py +1179 -0
  410. bootstack/widgets/avatar.py +140 -0
  411. bootstack/widgets/boolean_controls.py +455 -0
  412. bootstack/widgets/button.py +224 -0
  413. bootstack/widgets/buttongroup.py +239 -0
  414. bootstack/widgets/calendar.py +195 -0
  415. bootstack/widgets/card.py +159 -0
  416. bootstack/widgets/carousel.py +241 -0
  417. bootstack/widgets/chart.py +302 -0
  418. bootstack/widgets/codeeditor.py +675 -0
  419. bootstack/widgets/contextmenu.py +371 -0
  420. bootstack/widgets/datatable.py +688 -0
  421. bootstack/widgets/datefield.py +395 -0
  422. bootstack/widgets/divider.py +60 -0
  423. bootstack/widgets/expander.py +579 -0
  424. bootstack/widgets/form.py +200 -0
  425. bootstack/widgets/gallery.py +250 -0
  426. bootstack/widgets/gauge.py +168 -0
  427. bootstack/widgets/grid.py +121 -0
  428. bootstack/widgets/groupbox.py +162 -0
  429. bootstack/widgets/label.py +229 -0
  430. bootstack/widgets/listview.py +354 -0
  431. bootstack/widgets/menubutton.py +419 -0
  432. bootstack/widgets/numberfield.py +431 -0
  433. bootstack/widgets/pagestack.py +345 -0
  434. bootstack/widgets/passwordfield.py +383 -0
  435. bootstack/widgets/pathfield.py +454 -0
  436. bootstack/widgets/picture.py +251 -0
  437. bootstack/widgets/progressbar.py +106 -0
  438. bootstack/widgets/radio_variants.py +271 -0
  439. bootstack/widgets/radiogroup.py +228 -0
  440. bootstack/widgets/scrollbar.py +64 -0
  441. bootstack/widgets/scrollview.py +186 -0
  442. bootstack/widgets/select.py +302 -0
  443. bootstack/widgets/selectbutton.py +182 -0
  444. bootstack/widgets/sidebar_toggle.py +130 -0
  445. bootstack/widgets/sizegrip.py +42 -0
  446. bootstack/widgets/slider.py +408 -0
  447. bootstack/widgets/spinbox.py +147 -0
  448. bootstack/widgets/spinnerfield.py +413 -0
  449. bootstack/widgets/splash.py +367 -0
  450. bootstack/widgets/splitview.py +482 -0
  451. bootstack/widgets/stacks.py +249 -0
  452. bootstack/widgets/statusbar.py +189 -0
  453. bootstack/widgets/tabs.py +425 -0
  454. bootstack/widgets/textarea.py +459 -0
  455. bootstack/widgets/textfield.py +410 -0
  456. bootstack/widgets/theme_toggle.py +92 -0
  457. bootstack/widgets/timefield.py +340 -0
  458. bootstack/widgets/toast.py +314 -0
  459. bootstack/widgets/togglegroup.py +231 -0
  460. bootstack/widgets/toolbar.py +347 -0
  461. bootstack/widgets/tooltip.py +94 -0
  462. bootstack/widgets/tree.py +648 -0
  463. bootstack/widgets/types.py +305 -0
  464. bootstack/widgets/window.py +307 -0
  465. bootstack-0.1.0.dist-info/METADATA +301 -0
  466. bootstack-0.1.0.dist-info/RECORD +471 -0
  467. bootstack-0.1.0.dist-info/WHEEL +5 -0
  468. bootstack-0.1.0.dist-info/entry_points.txt +2 -0
  469. bootstack-0.1.0.dist-info/licenses/LICENSE +21 -0
  470. bootstack-0.1.0.dist-info/licenses/NOTICE +10 -0
  471. bootstack-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,860 @@
1
+ import math
2
+ from tkinter import Canvas, DoubleVar, IntVar, StringVar, font as tkfont
3
+ from typing import Any, Callable, Literal
4
+ from warnings import warn
5
+
6
+ from PIL import Image, ImageDraw, ImageTk
7
+ from PIL.Image import Resampling
8
+
9
+ from bootstack._core.exceptions import ConfigurationWarning
10
+ from bootstack.events import SliderEvent
11
+ from bootstack.widgets._impl.primitives.frame import Frame
12
+ from bootstack.widgets._impl.mixins.configure_mixin import configure_delegate
13
+ from bootstack.widgets.types import Master
14
+
15
+ DEFAULT_IMAGE_SCALE = 6
16
+
17
+
18
+ class Meter(Frame):
19
+ """A circular progress meter widget with customizable appearance and optional text display.
20
+
21
+ The Meter widget displays a value as a circular arc indicator with optional value text,
22
+ prefix/suffix labels, and subtitle. Supports both full circle and semi-circle styles,
23
+ segmented or solid indicators, and interactive mode for user input.
24
+
25
+ The meter value can be accessed and modified using:
26
+
27
+ - `get()` / `set(value)` - Standard value-widget API methods
28
+ - `.value` property - Direct property access
29
+ - `configure(value=x)` - Via the configure interface
30
+
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ master: Master = None,
36
+ accent: str = None,
37
+ surface: str = None,
38
+
39
+ # value parameters
40
+ value: int | float = 0,
41
+ minvalue: int | float = 0,
42
+ maxvalue: int | float = 100,
43
+ value_format: str = "{:.0f}",
44
+ value_prefix: str = None,
45
+ value_suffix: str = None,
46
+ value_font: str = None,
47
+ dtype: type[int] | type[float] = int,
48
+
49
+ # secondary options
50
+ secondary_font: str = None,
51
+ secondary_style: str = None,
52
+ subtitle: str = None,
53
+
54
+ # appearance
55
+ size: int = 200,
56
+ thickness: int = 10,
57
+ indicator_width: int = 0,
58
+ segment_width: int = 0,
59
+ arc_range: int = None,
60
+ arc_offset: int = None,
61
+
62
+ # other
63
+ meter_type: Literal['semi', 'full'] = 'full',
64
+ show_text: bool = True,
65
+ interactive: bool = False,
66
+ step_size: int | float = 1,
67
+ **kwargs: Any
68
+ ):
69
+ """Initialize a Meter widget.
70
+
71
+ Args:
72
+ master: The parent widget.
73
+ accent: Accent token for the meter indicator (e.g., 'primary', 'success').
74
+
75
+ value: Current meter value.
76
+ minvalue: Minimum value for the meter range.
77
+ maxvalue: Maximum value for the meter range.
78
+ value_format: Format string for displaying the value (e.g., "{:.0f}", "{:.2f}").
79
+ value_prefix: Text to display before the value (e.g., "$", "@").
80
+ value_suffix: Text to display after the value (e.g., "%", "mph").
81
+ value_font: Font specification for the value text (e.g., "-size 36 -weight bold").
82
+ dtype: Data type for the value variable (int or float).
83
+
84
+ secondary_font: Font specification for prefix, suffix, and subtitle text.
85
+ secondary_style: Style name for prefix, suffix, and subtitle color.
86
+ subtitle: Optional subtitle text displayed below the value.
87
+
88
+ size: Width and height of the meter in pixels.
89
+ thickness: Width of the meter arc in pixels.
90
+ indicator_width: Width of the indicator segment when using a wedge-style indicator.
91
+ 0 means fill from start to current value.
92
+ segment_width: Width of each segment for a segmented meter style. 0 means solid.
93
+ arc_range: Total arc range in degrees. None uses defaults (360 for full, 270 for semi).
94
+ arc_offset: Starting angle offset in degrees. None uses defaults (-90 for full, 135 for semi).
95
+
96
+ meter_type: Meter style - 'full' for full circle or 'semi' for semicircle.
97
+ show_text: Whether to display the value text and labels.
98
+ interactive: Whether the meter responds to mouse clicks/drags to change value.
99
+ step_size: Increment step when in interactive mode.
100
+ **kwargs: Additional keyword arguments passed to the Frame parent class.
101
+
102
+ """
103
+ super().__init__(master, **kwargs)
104
+
105
+ # configuration
106
+ self._dtype = dtype
107
+ self._size = size
108
+ self._thickness = thickness
109
+ self._indicator_width = indicator_width
110
+ self._segment_width = segment_width
111
+ self._arc_range = arc_range
112
+ self._arc_offset = arc_offset
113
+ self._minvalue = minvalue
114
+ self._maxvalue = maxvalue
115
+
116
+ self._meter_type = meter_type
117
+ self._show_text = show_text
118
+ self._interactive = interactive
119
+ self._step_size = step_size
120
+
121
+ self._value_format = value_format
122
+ self._value_font = value_font or '-size 36 -weight bold'
123
+ self._value_prefix = value_prefix
124
+ self._value_suffix = value_suffix
125
+ self._accent = accent or 'primary'
126
+
127
+ self._subtitle = subtitle
128
+ self._secondary_font = secondary_font or '-size 9'
129
+ self._secondary_style = secondary_style or 'background[muted]'
130
+
131
+ # color tokens (separate from resolved colors)
132
+ self._surface_token = surface or 'background'
133
+
134
+ # state tracking
135
+ self._towards_maximum = True
136
+ self._resolve_arc_range_offset(meter_type, arc_offset, arc_range)
137
+ self._binding = {}
138
+
139
+ # widget variables
140
+ self._last_changed_value = value
141
+ self._value_var = self._variable(value)
142
+ self._value_var.trace_add('write', self._update_meter) # Update meter when value changes
143
+ self._value_display_var = StringVar(value=value_format.format(value))
144
+ self._subtitle_var = StringVar(value=self._subtitle)
145
+
146
+ # Resolve styles first to get colors
147
+ self._resolve_meter_styles()
148
+
149
+ # layout - use canvas for both meter and text. Pass the surface *token*
150
+ # (not a resolved color) so the unified theme walk re-resolves the canvas
151
+ # background against the active theme on every change — a frozen hex would
152
+ # re-apply the old color forever.
153
+ self._canvas = Canvas(
154
+ master=self,
155
+ width=self._size,
156
+ height=self._size,
157
+ highlightthickness=0,
158
+ surface=self._surface_token,
159
+ )
160
+
161
+ # Canvas text items (will be created/updated in _draw_meter)
162
+ self._meter_image_id = None
163
+ self._value_text_id = None
164
+ self._prefix_text_id = None
165
+ self._suffix_text_id = None
166
+ self._subtitle_text_id = None
167
+
168
+ # The meter face / track / arc / text are painted imperatively onto the
169
+ # canvas (outside the ttk style loop), so the unified theme walk calls
170
+ # `_bs_apply_theme` to re-resolve + redraw them. The canvas *background*
171
+ # is handled by the walk's surface pass (the token above); the redraw owns
172
+ # only the painted content.
173
+ self._bind_interactive_events()
174
+ self._draw_base_meter_images()
175
+ self._draw_meter()
176
+
177
+ # set widget geometry
178
+ self._canvas.pack()
179
+
180
+ @property
181
+ def value(self):
182
+ return self._value_var.get()
183
+
184
+ @value.setter
185
+ def value(self, value: int | float):
186
+ self._value_var.set(value)
187
+
188
+ @property
189
+ def subtitle(self):
190
+ return self._subtitle_var.get()
191
+
192
+ @subtitle.setter
193
+ def subtitle(self, value: str):
194
+ self._subtitle = value
195
+ self._subtitle_var.set(value)
196
+
197
+ # ------ Value API Methods ------
198
+
199
+ def get(self):
200
+ """Return the current meter value.
201
+
202
+ This is part of the standard value-widget API. It is equivalent
203
+ to accessing the `.value` property.
204
+
205
+ Returns:
206
+ The current meter value (int or float depending on dtype).
207
+ """
208
+ return self.value
209
+
210
+ def set(self, value):
211
+ """Set the meter value.
212
+
213
+ This is part of the standard value-widget API. It is equivalent
214
+ to setting the `.value` property.
215
+
216
+ Args:
217
+ value: The new meter value.
218
+ """
219
+ self.value = value
220
+
221
+ # ------ Configuration Delegates ------
222
+
223
+ @configure_delegate('accent')
224
+ def _delegate_accent(self, value=None):
225
+ if value is None:
226
+ return self._accent
227
+ else:
228
+ self._accent = value
229
+ self._resolve_meter_styles()
230
+ self._draw_meter()
231
+ return None
232
+
233
+ @configure_delegate('value')
234
+ def _delegate_value(self, value=None):
235
+ if value is None:
236
+ return self.value
237
+ else:
238
+ self.value = value
239
+ return None
240
+
241
+ @configure_delegate('minvalue')
242
+ def _delegate_minvalue(self, value=None):
243
+ if value is None:
244
+ return self._minvalue
245
+ else:
246
+ self._minvalue = value
247
+ self._draw_meter()
248
+ return None
249
+
250
+ @configure_delegate('maxvalue')
251
+ def _delegate_maxvalue(self, value=None):
252
+ if value is None:
253
+ return self._maxvalue
254
+ else:
255
+ self._maxvalue = value
256
+ self._draw_meter()
257
+ return None
258
+
259
+ @configure_delegate('value_format')
260
+ def _delegate_value_format(self, value=None):
261
+ if value is None:
262
+ return self._value_format
263
+ else:
264
+ self._value_format = value
265
+ self._draw_meter()
266
+ return None
267
+
268
+ @configure_delegate('value_prefix')
269
+ def _delegate_value_prefix(self, value=None):
270
+ if value is None:
271
+ return self._value_prefix
272
+ else:
273
+ self._value_prefix = value
274
+ self._draw_meter()
275
+ return None
276
+
277
+ @configure_delegate('value_suffix')
278
+ def _delegate_value_suffix(self, value=None):
279
+ if value is None:
280
+ return self._value_suffix
281
+ else:
282
+ self._value_suffix = value
283
+ self._draw_meter()
284
+ return None
285
+
286
+ @configure_delegate('value_font')
287
+ def _delegate_value_font(self, value=None):
288
+ if value is None:
289
+ return self._value_font
290
+ else:
291
+ self._value_font = value
292
+ self._draw_meter()
293
+ return None
294
+
295
+ @configure_delegate('dtype')
296
+ def _delegate_dtype(self, value=None):
297
+ if value is None:
298
+ return self._dtype
299
+ else:
300
+ warn('dtype is only configurable in the widget constructor', ConfigurationWarning)
301
+ return None
302
+
303
+ @configure_delegate('secondary_font')
304
+ def _delegate_secondary_font(self, value=None):
305
+ if value is None:
306
+ return self._secondary_font
307
+ else:
308
+ self._secondary_font = value
309
+ self._draw_meter()
310
+ return None
311
+
312
+ @configure_delegate('secondary_style')
313
+ def _delegate_secondary_style(self, value=None):
314
+ if value is None:
315
+ return self._secondary_style
316
+ else:
317
+ self._secondary_style = value
318
+ self._resolve_meter_styles()
319
+ self._draw_meter()
320
+ return None
321
+
322
+ @configure_delegate('subtitle')
323
+ def _delegate_subtitle(self, value=None):
324
+ if value is None:
325
+ return self.subtitle
326
+ else:
327
+ self.subtitle = value
328
+ self._draw_meter()
329
+ return None
330
+
331
+ @configure_delegate('size')
332
+ def _delegate_size(self, value=None):
333
+ if value is None:
334
+ return self._size
335
+ else:
336
+ self._size = value
337
+ self._canvas.configure(width=value, height=value)
338
+ self._draw_base_meter_images()
339
+ self._draw_meter()
340
+ return None
341
+
342
+ @configure_delegate('thickness')
343
+ def _delegate_thickness(self, value=None):
344
+ if value is None:
345
+ return self._thickness
346
+ else:
347
+ self._thickness = value
348
+ self._draw_base_meter_images()
349
+ self._draw_meter()
350
+ return None
351
+
352
+ @configure_delegate('indicator_width')
353
+ def _delegate_indicator_width(self, value=None):
354
+ if value is None:
355
+ return self._indicator_width
356
+ else:
357
+ self._indicator_width = value
358
+ self._draw_meter()
359
+ return None
360
+
361
+ @configure_delegate('segment_width')
362
+ def _delegate_segment_width(self, value=None):
363
+ if value is None:
364
+ return self._segment_width
365
+ else:
366
+ self._segment_width = value
367
+ self._draw_base_meter_images()
368
+ self._draw_meter()
369
+ return None
370
+
371
+ @configure_delegate('arc_range')
372
+ def _delegate_arc_range(self, value=None):
373
+ if value is None:
374
+ return self._arc_range
375
+ else:
376
+ self._arc_range = value
377
+ self._draw_base_meter_images()
378
+ self._draw_meter()
379
+ return None
380
+
381
+ @configure_delegate('arc_offset')
382
+ def _delegate_arc_offset(self, value=None):
383
+ if value is None:
384
+ return self._arc_offset
385
+ else:
386
+ self._arc_offset = value
387
+ self._draw_base_meter_images()
388
+ self._draw_meter()
389
+ return None
390
+
391
+ @configure_delegate('meter_type')
392
+ def _delegate_meter_type(self, value=None):
393
+ if value is None:
394
+ return self._meter_type
395
+ else:
396
+ self._resolve_arc_range_offset(value, self._arc_offset, self._arc_range)
397
+ self._draw_base_meter_images()
398
+ self._draw_meter()
399
+ return None
400
+
401
+ @configure_delegate('show_text')
402
+ def _delegate_show_text(self, value=None):
403
+ if value is None:
404
+ return self._show_text
405
+ else:
406
+ self._show_text = value
407
+ self._draw_meter()
408
+ return None
409
+
410
+ @configure_delegate('interactive')
411
+ def _delegate_interactive(self, value=None):
412
+ if value is None:
413
+ return self._interactive
414
+ else:
415
+ self._interactive = value
416
+ self._bind_interactive_events()
417
+ return None
418
+
419
+ @configure_delegate('step_size')
420
+ def _delegate_step_size(self, value=None):
421
+ if value is None:
422
+ return self._step_size
423
+ else:
424
+ self._step_size = value
425
+ return None
426
+
427
+ def _variable(self, value: int | float):
428
+ return IntVar(value=value) if self._dtype is int else DoubleVar(value=value)
429
+
430
+ def _update_meter(self, *_: Any):
431
+ """Update meter display when value changes."""
432
+ value = self._value_var.get()
433
+ self._value_display_var.set(self._value_format.format(value))
434
+ self._draw_meter()
435
+ if value != self._last_changed_value:
436
+ prev_value = self._last_changed_value
437
+ self._last_changed_value = value
438
+ self.event_generate('<<Change>>', data=SliderEvent(value=value, prev_value=prev_value))
439
+
440
+ def _resolve_meter_styles(self):
441
+ """Resolve theme colors for meter indicator, trough, and text."""
442
+ from bootstack.style.style import get_style
443
+ style = get_style()
444
+ b = style.style_builder
445
+
446
+ # Resolve colors from tokens
447
+ accent_token = self._accent or 'primary'
448
+ accent_color = b.color(accent_token)
449
+
450
+ # Use _surface_token to get the token name, resolve to actual color
451
+ surface_token = getattr(self, '_surface_token', 'background')
452
+ surface = b.color(surface_token)
453
+ trough_color = b.border(surface)
454
+
455
+ # Get text colors
456
+ value_text_color = b.color(accent_token)
457
+ secondary_text_color = b.color(self._secondary_style or 'background[muted]')
458
+
459
+ # Supersample factor for antialiasing: the meter is drawn at
460
+ # `size * image_scale` then downscaled to the gauge's *logical* size, so
461
+ # the extra resolution `b.scale()` adds for the display's DPI is thrown
462
+ # away by the downscale — pure waste that balloons the redraw on HiDPI
463
+ # (a 200px gauge at 2x DPI would supersample to 2400px²). Cap it at the
464
+ # base factor: identical output at standard DPI, ~2-4x cheaper on HiDPI.
465
+ self._image_scale = min(b.scale(DEFAULT_IMAGE_SCALE), DEFAULT_IMAGE_SCALE)
466
+ self._accent_color = accent_color
467
+ self._surface = surface # Store resolved color
468
+ self._trough_color = trough_color
469
+ self._value_text_color = value_text_color
470
+ self._secondary_text_color = secondary_text_color
471
+
472
+ def _bind_interactive_events(self):
473
+ seq1 = '<B1-Motion>'
474
+ seq2 = '<Button-1>'
475
+
476
+ if self._interactive:
477
+ self._binding[seq1] = self._canvas.bind(seq1, self._handle_interaction)
478
+ self._binding[seq2] = self._canvas.bind(seq2, self._handle_interaction)
479
+ return
480
+
481
+ if seq1 in self._binding:
482
+ self._canvas.unbind(seq1, self._binding[seq1])
483
+ self._canvas.unbind(seq2, self._binding[seq2])
484
+ self._binding.clear()
485
+
486
+ def _resolve_arc_range_offset(self, meter_type: str, arc_offset: int | None, arc_range: int | None):
487
+ """Set default arc parameters based on meter type (full or semi)."""
488
+ if meter_type == 'semi':
489
+ self._arc_offset = 135 if arc_offset is None else arc_offset
490
+ self._arc_range = 270 if arc_range is None else arc_range
491
+ else:
492
+ self._arc_offset = -90 if arc_offset is None else arc_offset
493
+ self._arc_range = 360 if arc_range is None else arc_range
494
+ self._meter_type = meter_type
495
+
496
+ def _draw_meter(self):
497
+ """Draw meter indicator and text on canvas."""
498
+ # Draw meter indicator
499
+ img = self._base_image.copy()
500
+ draw = ImageDraw.Draw(img)
501
+ if self._segment_width > 0:
502
+ self._draw_segment_indicator(draw)
503
+ else:
504
+ self._draw_solid_indicator(draw)
505
+
506
+ self._meter_image = ImageTk.PhotoImage(
507
+ img.resize(
508
+ (self._size, self._size),
509
+ Resampling.BILINEAR
510
+ )
511
+ )
512
+
513
+ # Update or create image on canvas
514
+ if self._meter_image_id is None:
515
+ self._meter_image_id = self._canvas.create_image(
516
+ 0, 0,
517
+ image=self._meter_image,
518
+ anchor='nw'
519
+ )
520
+ else:
521
+ self._canvas.itemconfig(self._meter_image_id, image=self._meter_image)
522
+
523
+ # Draw text if enabled
524
+ if self._show_text:
525
+ self._draw_text_on_canvas()
526
+ else:
527
+ # Hide text elements
528
+ self._hide_text_items()
529
+ # Show subtitle if it exists
530
+ if self._subtitle:
531
+ self._draw_subtitle_centered()
532
+
533
+ def _draw_text_on_canvas(self):
534
+ """Draw value text with optional prefix, suffix, and subtitle on canvas."""
535
+ value_text = self._value_display_var.get()
536
+ center_x = self._size / 2
537
+ center_y = self._size / 2
538
+
539
+ # Create font objects once
540
+ value_font = tkfont.Font(font=self._value_font)
541
+ secondary_font = tkfont.Font(font=self._secondary_font)
542
+
543
+ # Get font metrics
544
+ value_metrics = value_font.metrics()
545
+ secondary_metrics = secondary_font.metrics()
546
+
547
+ value_height = value_metrics['ascent'] + value_metrics['descent']
548
+ secondary_height = secondary_metrics['ascent'] + secondary_metrics['descent']
549
+
550
+ # Calculate max height of value line (value + prefix/suffix)
551
+ max_text_height = max(value_height, secondary_height) if (
552
+ self._value_prefix or self._value_suffix) else value_height
553
+
554
+ # Calculate total block height including subtitle
555
+ subtitle_height = secondary_height if self._subtitle else 0
556
+ subtitle_gap = -4 if self._subtitle else 0
557
+ total_height = max_text_height + subtitle_gap + subtitle_height
558
+
559
+ # Position value text to center the entire block
560
+ block_top = center_y - (total_height / 2)
561
+ value_y = block_top + (max_text_height / 2)
562
+
563
+ # Calculate value text baseline for prefix/suffix alignment
564
+ value_baseline_y = value_y + (value_metrics['ascent'] - value_metrics['descent']) / 2
565
+
566
+ # Draw value text
567
+ self._value_text_id = self._update_or_create_text(
568
+ self._value_text_id, center_x, value_y,
569
+ value_text, self._value_font, self._value_text_color, 'center'
570
+ )
571
+
572
+ # Position and draw prefix/suffix
573
+ if self._value_prefix or self._value_suffix:
574
+ value_width = value_font.measure(value_text)
575
+ horizontal_gap = 4
576
+ value_left_x = center_x - (value_width / 2) - horizontal_gap
577
+ value_right_x = center_x + (value_width / 2) + horizontal_gap - 1
578
+
579
+ # Calculate y position for prefix/suffix (baseline aligned, slightly raised)
580
+ secondary_y = value_baseline_y - (secondary_metrics['ascent'] - secondary_metrics['descent']) / 2 - 4
581
+
582
+ if self._value_prefix:
583
+ self._prefix_text_id = self._update_or_create_text(
584
+ self._prefix_text_id, value_left_x, secondary_y,
585
+ self._value_prefix, self._secondary_font, self._secondary_text_color, 'e'
586
+ )
587
+ elif self._prefix_text_id:
588
+ self._canvas.itemconfig(self._prefix_text_id, state='hidden')
589
+
590
+ if self._value_suffix:
591
+ self._suffix_text_id = self._update_or_create_text(
592
+ self._suffix_text_id, value_right_x, secondary_y,
593
+ self._value_suffix, self._secondary_font, self._secondary_text_color, 'w'
594
+ )
595
+ elif self._suffix_text_id:
596
+ self._canvas.itemconfig(self._suffix_text_id, state='hidden')
597
+
598
+ # Draw subtitle
599
+ if self._subtitle:
600
+ subtitle_y = value_y + (max_text_height / 2) - 4
601
+ self._subtitle_text_id = self._update_or_create_text(
602
+ self._subtitle_text_id, center_x, subtitle_y,
603
+ self._subtitle_var.get(), self._secondary_font, self._secondary_text_color, 'n'
604
+ )
605
+ elif self._subtitle_text_id:
606
+ self._canvas.itemconfig(self._subtitle_text_id, state='hidden')
607
+
608
+ def _update_or_create_text(self, item_id, x, y, text, font, fill, anchor):
609
+ """Update existing canvas text item or create new one.
610
+
611
+ Args:
612
+ item_id: Existing canvas item ID or None to create new.
613
+ x: X coordinate for text position.
614
+ y: Y coordinate for text position.
615
+ text: Text string to display.
616
+ font: Font specification.
617
+ fill: Text color.
618
+ anchor: Text anchor position (e.g., 'center', 'e', 'w').
619
+
620
+ Returns:
621
+ Canvas item ID.
622
+ """
623
+ if item_id is None:
624
+ return self._canvas.create_text(x, y, text=text, font=font, fill=fill, anchor=anchor)
625
+ else:
626
+ self._canvas.itemconfig(item_id, text=text, font=font, fill=fill, state='normal')
627
+ self._canvas.coords(item_id, x, y)
628
+ return item_id
629
+
630
+ def _hide_text_items(self):
631
+ """Hide all text items on canvas."""
632
+ if self._value_text_id:
633
+ self._canvas.itemconfig(self._value_text_id, state='hidden')
634
+ if self._prefix_text_id:
635
+ self._canvas.itemconfig(self._prefix_text_id, state='hidden')
636
+ if self._suffix_text_id:
637
+ self._canvas.itemconfig(self._suffix_text_id, state='hidden')
638
+ if self._subtitle_text_id:
639
+ self._canvas.itemconfig(self._subtitle_text_id, state='hidden')
640
+
641
+ def _draw_subtitle_centered(self):
642
+ """Draw subtitle centered when show_text is False."""
643
+ center_x = self._size // 2
644
+ center_y = self._size // 2
645
+
646
+ if self._subtitle_text_id is None:
647
+ self._subtitle_text_id = self._canvas.create_text(
648
+ center_x, center_y,
649
+ text=self._subtitle_var.get(),
650
+ font=self._secondary_font,
651
+ fill=self._secondary_text_color,
652
+ anchor='center'
653
+ )
654
+ else:
655
+ self._canvas.itemconfig(
656
+ self._subtitle_text_id,
657
+ text=self._subtitle_var.get(),
658
+ font=self._secondary_font,
659
+ fill=self._secondary_text_color,
660
+ state='normal'
661
+ )
662
+ self._canvas.coords(self._subtitle_text_id, center_x, center_y)
663
+
664
+ def _draw_base_meter_images(self):
665
+ """Draw meter background trough at high resolution."""
666
+ self._resolve_meter_styles()
667
+ self._base_image = Image.new(
668
+ mode='RGBA',
669
+ size=(self._size * self._image_scale, self._size * self._image_scale),
670
+ )
671
+ draw = ImageDraw.Draw(self._base_image)
672
+
673
+ # Center the arc with equal margins on all sides
674
+ margin = 10
675
+ x1 = y1 = self._size * self._image_scale - margin
676
+ width = self._thickness * self._image_scale
677
+
678
+ if self._segment_width > 0:
679
+ # segmented meter
680
+ minvalue = self._arc_offset
681
+ maxvalue = self._arc_range + self._arc_offset
682
+ step = 2 if self._segment_width == 1 else self._segment_width
683
+
684
+ for x in range(minvalue, maxvalue, step):
685
+ draw.arc(
686
+ xy=(margin, margin, x1, y1),
687
+ start=x,
688
+ end=x + self._segment_width - 1,
689
+ fill=self._trough_color,
690
+ width=width
691
+ )
692
+ else:
693
+ # default meter
694
+ draw.arc(
695
+ xy=(margin, margin, x1, y1),
696
+ start=self._arc_offset,
697
+ end=self._arc_range + self._arc_offset,
698
+ fill=self._trough_color,
699
+ width=width
700
+ )
701
+
702
+ def _draw_solid_indicator(self, draw):
703
+ """Draw solid arc indicator from start to current value."""
704
+ margin = 10
705
+ x1 = y1 = self._size * self._image_scale - margin
706
+ width = self._thickness * self._image_scale
707
+ value_degrees = self._meter_value_as_degrees()
708
+
709
+ if self._indicator_width > 0:
710
+ draw.arc(
711
+ xy=(margin, margin, x1, y1),
712
+ start=value_degrees - self._indicator_width,
713
+ end=value_degrees + self._indicator_width,
714
+ fill=self._accent_color,
715
+ width=width
716
+ )
717
+ else:
718
+ draw.arc(
719
+ xy=(margin, margin, x1, y1),
720
+ start=self._arc_offset,
721
+ end=value_degrees,
722
+ fill=self._accent_color,
723
+ width=width
724
+ )
725
+
726
+ def _draw_segment_indicator(self, draw):
727
+ """Draw segmented arc indicator from start to current value."""
728
+ value_degrees = self._meter_value_as_degrees()
729
+ margin = 10
730
+ x1 = y1 = self._size * self._image_scale - margin
731
+ width = self._thickness * self._image_scale
732
+
733
+ if self._indicator_width > 0:
734
+ draw.arc(
735
+ xy=(margin, margin, x1, y1),
736
+ start=value_degrees - self._indicator_width,
737
+ end=value_degrees + self._indicator_width,
738
+ fill=self._accent_color,
739
+ width=width
740
+ )
741
+ else:
742
+ # Draw segments from arc start to current value in degrees
743
+ minvalue = self._arc_offset
744
+ maxvalue = value_degrees - 1
745
+ step = self._segment_width
746
+
747
+ for x in range(minvalue, maxvalue, step):
748
+ draw.arc(
749
+ xy=(margin, margin, x1, y1),
750
+ start=x,
751
+ end=x + self._segment_width - 1,
752
+ fill=self._accent_color,
753
+ width=width
754
+ )
755
+
756
+ def _meter_value_as_degrees(self):
757
+ """Convert current meter value to arc degrees.
758
+
759
+ Returns:
760
+ Degree value for meter indicator position.
761
+ """
762
+ minvalue = self._minvalue
763
+ maxvalue = self._maxvalue
764
+ value = self._value_var.get()
765
+
766
+ # normalize to 0-1 range to handle negative values
767
+ range_size = maxvalue - minvalue
768
+ if range_size == 0:
769
+ normalized = 0
770
+ else:
771
+ normalized = (value - minvalue) / range_size
772
+
773
+ return int(normalized * self._arc_range + self._arc_offset)
774
+
775
+ def _bs_apply_theme(self):
776
+ """Re-resolve theme colors and repaint the painted content (face + arcs +
777
+ text). Called by the unified theme walk when this gauge is on screen; the
778
+ canvas *background* is recolored separately by the walk's surface pass.
779
+ """
780
+ self._resolve_meter_styles()
781
+ self._draw_base_meter_images()
782
+ self._draw_meter()
783
+
784
+ def _handle_interaction(self, e):
785
+ """Handle mouse clicks/drags to update meter value in interactive mode."""
786
+ dx = e.x - self._size // 2
787
+ dy = e.y - self._size // 2
788
+ rads = math.atan2(dy, dx)
789
+ degrees = math.degrees(rads)
790
+
791
+ if degrees > self._arc_offset:
792
+ factor = degrees - self._arc_offset
793
+ else:
794
+ factor = 360 + degrees - self._arc_offset
795
+
796
+ # clamp the value between `minvalue` and `maxvalue`
797
+ minvalue = self._minvalue
798
+ maxvalue = self._maxvalue
799
+ last_value = self._value_var.get()
800
+
801
+ # calculate the value based on the range
802
+ range_size = maxvalue - minvalue
803
+ value = (range_size / self._arc_range * factor) + minvalue
804
+
805
+ # calculate the value given the stepsize.
806
+ if self._step_size > 0:
807
+ # round to the nearest stepsize
808
+ value = round(value / self._step_size) * self._step_size
809
+
810
+ # if the number is the same, then do not redraw
811
+ if last_value == value:
812
+ return
813
+
814
+ # update the value variable
815
+ self._value_var.set(max(min(value, maxvalue), minvalue))
816
+
817
+ def step(self, delta: int | float = 1):
818
+ """Increment or decrement meter value with automatic bounce at limits.
819
+
820
+ Args:
821
+ delta: Amount to step by (default 1).
822
+ """
823
+ value = self._value_var.get()
824
+ minvalue = self._minvalue
825
+ maxvalue = self._maxvalue
826
+
827
+ if self._towards_maximum:
828
+ value_updated = value + delta
829
+ else:
830
+ value_updated = value - delta
831
+
832
+ if value_updated >= maxvalue:
833
+ self._towards_maximum = False
834
+ self._value_var.set(maxvalue - (value_updated - maxvalue))
835
+ elif value_updated < minvalue:
836
+ self._towards_maximum = True
837
+ self._value_var.set(minvalue + (minvalue - value_updated))
838
+ else:
839
+ self._value_var.set(value_updated)
840
+
841
+ def on_changed(self, callback: Callable[[Any], Any]) -> str:
842
+ """Register a callback for meter value changes.
843
+
844
+ Args:
845
+ callback: Called with a `SliderEvent`; `event.value` is the
846
+ new value, `event.prev_value` is the previous value.
847
+
848
+ Returns:
849
+ Bind ID — pass to `off_changed()` to unregister.
850
+ """
851
+ return self.bind('<<Change>>', callback, add="+")
852
+
853
+ def off_changed(self, bind_id: str) -> None:
854
+ """Unregister a callback registered with `on_changed()`.
855
+
856
+ Args:
857
+ bind_id: The bind ID returned by `on_changed()`.
858
+ """
859
+ self.unbind('<<Change>>', bind_id)
860
+