bootstack 0.1.0a1__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 (419) hide show
  1. bootstack/__init__.py +249 -0
  2. bootstack/__main__.py +5 -0
  3. bootstack/api/__init__.py +127 -0
  4. bootstack/api/app.py +30 -0
  5. bootstack/api/constants.py +3 -0
  6. bootstack/api/data.py +23 -0
  7. bootstack/api/dialogs.py +44 -0
  8. bootstack/api/i18n.py +17 -0
  9. bootstack/api/localization.py +16 -0
  10. bootstack/api/menu.py +7 -0
  11. bootstack/api/style.py +23 -0
  12. bootstack/api/utils.py +24 -0
  13. bootstack/api/widgets.py +137 -0
  14. bootstack/assets/__init__.py +24 -0
  15. bootstack/assets/bootstack-transparent.png +0 -0
  16. bootstack/assets/bootstack.ico +0 -0
  17. bootstack/assets/bootstack.png +0 -0
  18. bootstack/assets/elements/__init__.py +0 -0
  19. bootstack/assets/elements/badge-pill.png +0 -0
  20. bootstack/assets/elements/badge-square.png +0 -0
  21. bootstack/assets/elements/border.png +0 -0
  22. bootstack/assets/elements/button-compact.png +0 -0
  23. bootstack/assets/elements/button-default.png +0 -0
  24. bootstack/assets/elements/button-group-horizontal-after-compact.png +0 -0
  25. bootstack/assets/elements/button-group-horizontal-after-default.png +0 -0
  26. bootstack/assets/elements/button-group-horizontal-before-compact.png +0 -0
  27. bootstack/assets/elements/button-group-horizontal-before-default.png +0 -0
  28. bootstack/assets/elements/button-group-horizontal-center-compact.png +0 -0
  29. bootstack/assets/elements/button-group-horizontal-center-default.png +0 -0
  30. bootstack/assets/elements/button-group-vertical-after-compact.png +0 -0
  31. bootstack/assets/elements/button-group-vertical-after-default.png +0 -0
  32. bootstack/assets/elements/button-group-vertical-before-compact.png +0 -0
  33. bootstack/assets/elements/button-group-vertical-before-default.png +0 -0
  34. bootstack/assets/elements/button-group-vertical-center-compact.png +0 -0
  35. bootstack/assets/elements/button-group-vertical-center-default.png +0 -0
  36. bootstack/assets/elements/checkbox-checked.png +0 -0
  37. bootstack/assets/elements/checkbox-indeterminate.png +0 -0
  38. bootstack/assets/elements/checkbox-unchecked.png +0 -0
  39. bootstack/assets/elements/field.png +0 -0
  40. bootstack/assets/elements/input-after-compact.png +0 -0
  41. bootstack/assets/elements/input-after-default.png +0 -0
  42. bootstack/assets/elements/input-before-compact.png +0 -0
  43. bootstack/assets/elements/input-before-default.png +0 -0
  44. bootstack/assets/elements/input-compact.png +0 -0
  45. bootstack/assets/elements/input-default.png +0 -0
  46. bootstack/assets/elements/list-item-separated.png +0 -0
  47. bootstack/assets/elements/list-item.png +0 -0
  48. bootstack/assets/elements/manifest.toml +480 -0
  49. bootstack/assets/elements/menu-item.png +0 -0
  50. bootstack/assets/elements/nav-button-compact.png +0 -0
  51. bootstack/assets/elements/nav-button-default.png +0 -0
  52. bootstack/assets/elements/nav-icon-button-compact.png +0 -0
  53. bootstack/assets/elements/nav-icon-button-default.png +0 -0
  54. bootstack/assets/elements/notebook-client-border.png +0 -0
  55. bootstack/assets/elements/notebook-tab-active.png +0 -0
  56. bootstack/assets/elements/notebook-tab-bar.png +0 -0
  57. bootstack/assets/elements/notebook-tab-normal.png +0 -0
  58. bootstack/assets/elements/notebook-tab-pill.png +0 -0
  59. bootstack/assets/elements/progress-bar-horizontal-striped.png +0 -0
  60. bootstack/assets/elements/progress-bar-solid.png +0 -0
  61. bootstack/assets/elements/progress-bar-thin.png +0 -0
  62. bootstack/assets/elements/progress-bar-vertical-striped.png +0 -0
  63. bootstack/assets/elements/radio-selected.png +0 -0
  64. bootstack/assets/elements/radio-unselected.png +0 -0
  65. bootstack/assets/elements/scrollbar-horizontal.png +0 -0
  66. bootstack/assets/elements/scrollbar-vertical.png +0 -0
  67. bootstack/assets/elements/slider-handle-focus.png +0 -0
  68. bootstack/assets/elements/slider-handle.png +0 -0
  69. bootstack/assets/elements/slider-track-horizontal.png +0 -0
  70. bootstack/assets/elements/slider-track-vertical.png +0 -0
  71. bootstack/assets/elements/switch-off.png +0 -0
  72. bootstack/assets/elements/switch-on.png +0 -0
  73. bootstack/assets/elements/tabs-bar-horizontal.png +0 -0
  74. bootstack/assets/elements/tabs-bar-vertical.png +0 -0
  75. bootstack/assets/elements/tabs-pill.png +0 -0
  76. bootstack/assets/locales/ar/LC_MESSAGES/bootstack.mo +0 -0
  77. bootstack/assets/locales/ar/LC_MESSAGES/bootstack.po +853 -0
  78. bootstack/assets/locales/bg/LC_MESSAGES/bootstack.po +875 -0
  79. bootstack/assets/locales/cs/LC_MESSAGES/bootstack.mo +0 -0
  80. bootstack/assets/locales/cs/LC_MESSAGES/bootstack.po +853 -0
  81. bootstack/assets/locales/da/LC_MESSAGES/bootstack.mo +0 -0
  82. bootstack/assets/locales/da/LC_MESSAGES/bootstack.po +853 -0
  83. bootstack/assets/locales/de/LC_MESSAGES/bootstack.mo +0 -0
  84. bootstack/assets/locales/de/LC_MESSAGES/bootstack.po +853 -0
  85. bootstack/assets/locales/en/LC_MESSAGES/bootstack.mo +0 -0
  86. bootstack/assets/locales/en/LC_MESSAGES/bootstack.po +875 -0
  87. bootstack/assets/locales/es/LC_MESSAGES/bootstack.mo +0 -0
  88. bootstack/assets/locales/es/LC_MESSAGES/bootstack.po +853 -0
  89. bootstack/assets/locales/fr/LC_MESSAGES/bootstack.mo +0 -0
  90. bootstack/assets/locales/fr/LC_MESSAGES/bootstack.po +853 -0
  91. bootstack/assets/locales/he/LC_MESSAGES/bootstack.mo +0 -0
  92. bootstack/assets/locales/he/LC_MESSAGES/bootstack.po +851 -0
  93. bootstack/assets/locales/hi/LC_MESSAGES/bootstack.mo +0 -0
  94. bootstack/assets/locales/hi/LC_MESSAGES/bootstack.po +842 -0
  95. bootstack/assets/locales/it/LC_MESSAGES/bootstack.mo +0 -0
  96. bootstack/assets/locales/it/LC_MESSAGES/bootstack.po +841 -0
  97. bootstack/assets/locales/ja/LC_MESSAGES/bootstack.mo +0 -0
  98. bootstack/assets/locales/ja/LC_MESSAGES/bootstack.po +914 -0
  99. bootstack/assets/locales/ko/LC_MESSAGES/bootstack.mo +0 -0
  100. bootstack/assets/locales/ko/LC_MESSAGES/bootstack.po +842 -0
  101. bootstack/assets/locales/nb/LC_MESSAGES/bootstack.mo +0 -0
  102. bootstack/assets/locales/nb/LC_MESSAGES/bootstack.po +841 -0
  103. bootstack/assets/locales/nl/LC_MESSAGES/bootstack.mo +0 -0
  104. bootstack/assets/locales/nl/LC_MESSAGES/bootstack.po +841 -0
  105. bootstack/assets/locales/pl/LC_MESSAGES/bootstack.mo +0 -0
  106. bootstack/assets/locales/pl/LC_MESSAGES/bootstack.po +842 -0
  107. bootstack/assets/locales/pt/LC_MESSAGES/bootstack.mo +0 -0
  108. bootstack/assets/locales/pt/LC_MESSAGES/bootstack.po +842 -0
  109. bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.mo +0 -0
  110. bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.po +842 -0
  111. bootstack/assets/locales/sl/LC_MESSAGES/bootstack.mo +0 -0
  112. bootstack/assets/locales/sl/LC_MESSAGES/bootstack.po +842 -0
  113. bootstack/assets/locales/sv/LC_MESSAGES/bootstack.mo +0 -0
  114. bootstack/assets/locales/sv/LC_MESSAGES/bootstack.po +842 -0
  115. bootstack/assets/locales/tr/LC_MESSAGES/bootstack.mo +0 -0
  116. bootstack/assets/locales/tr/LC_MESSAGES/bootstack.po +842 -0
  117. bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.mo +0 -0
  118. bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.po +842 -0
  119. bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.mo +0 -0
  120. bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.po +842 -0
  121. bootstack/assets/themes/__init__.py +0 -0
  122. bootstack/assets/themes/amber-dark.json +32 -0
  123. bootstack/assets/themes/amber-light.json +32 -0
  124. bootstack/assets/themes/aurora-dark.json +32 -0
  125. bootstack/assets/themes/aurora-light.json +32 -0
  126. bootstack/assets/themes/bootstrap-dark.json +32 -0
  127. bootstack/assets/themes/bootstrap-light.json +32 -0
  128. bootstack/assets/themes/classic-dark.json +32 -0
  129. bootstack/assets/themes/classic-light.json +32 -0
  130. bootstack/assets/themes/docs-dark.json +32 -0
  131. bootstack/assets/themes/docs-light.json +32 -0
  132. bootstack/assets/themes/forest-dark.json +32 -0
  133. bootstack/assets/themes/forest-light.json +32 -0
  134. bootstack/assets/themes/ocean-dark.json +32 -0
  135. bootstack/assets/themes/ocean-light.json +32 -0
  136. bootstack/assets/themes/rose-dark.json +32 -0
  137. bootstack/assets/themes/rose-light.json +32 -0
  138. bootstack/assets/widgets/__init__.py +0 -0
  139. bootstack/assets/widgets/badge-default.png +0 -0
  140. bootstack/assets/widgets/badge-pill.png +0 -0
  141. bootstack/assets/widgets/border.png +0 -0
  142. bootstack/assets/widgets/button-group-horizontal-after.png +0 -0
  143. bootstack/assets/widgets/button-group-horizontal-before.png +0 -0
  144. bootstack/assets/widgets/button-group-horizontal-center.png +0 -0
  145. bootstack/assets/widgets/button-group-vertical-after.png +0 -0
  146. bootstack/assets/widgets/button-group-vertical-before.png +0 -0
  147. bootstack/assets/widgets/button-group-vertical-center.png +0 -0
  148. bootstack/assets/widgets/button.png +0 -0
  149. bootstack/assets/widgets/checkbox-checked.png +0 -0
  150. bootstack/assets/widgets/checkbox-indeterminate.png +0 -0
  151. bootstack/assets/widgets/checkbox-unchecked.png +0 -0
  152. bootstack/assets/widgets/field.png +0 -0
  153. bootstack/assets/widgets/icon-button.png +0 -0
  154. bootstack/assets/widgets/input-inner.png +0 -0
  155. bootstack/assets/widgets/input-prefix.png +0 -0
  156. bootstack/assets/widgets/input-suffix.png +0 -0
  157. bootstack/assets/widgets/input.png +0 -0
  158. bootstack/assets/widgets/list-item-focus.png +0 -0
  159. bootstack/assets/widgets/list-item-separated.png +0 -0
  160. bootstack/assets/widgets/menu-item-separated.png +0 -0
  161. bootstack/assets/widgets/notebook-client-border.png +0 -0
  162. bootstack/assets/widgets/notebook-pill-active.png +0 -0
  163. bootstack/assets/widgets/notebook-pill-inactive.png +0 -0
  164. bootstack/assets/widgets/notebook-tab-active.png +0 -0
  165. bootstack/assets/widgets/notebook-tab-border.png +0 -0
  166. bootstack/assets/widgets/notebook-tab-normal.png +0 -0
  167. bootstack/assets/widgets/notebook-underline.png +0 -0
  168. bootstack/assets/widgets/progress-bar-horizontal-default.png +0 -0
  169. bootstack/assets/widgets/progress-bar-horizontal-striped.png +0 -0
  170. bootstack/assets/widgets/progress-bar-vertical-default.png +0 -0
  171. bootstack/assets/widgets/progress-bar-vertical-striped.png +0 -0
  172. bootstack/assets/widgets/progress-trough-horizontal.png +0 -0
  173. bootstack/assets/widgets/progress-trough-vertical.png +0 -0
  174. bootstack/assets/widgets/radio-selected.png +0 -0
  175. bootstack/assets/widgets/radio-unselected.png +0 -0
  176. bootstack/assets/widgets/scrollbar-horizontal-rounded.png +0 -0
  177. bootstack/assets/widgets/scrollbar-vertical-rounded.png +0 -0
  178. bootstack/assets/widgets/separator-horizontal.png +0 -0
  179. bootstack/assets/widgets/separator-vertical.png +0 -0
  180. bootstack/assets/widgets/slider-handle-focus.png +0 -0
  181. bootstack/assets/widgets/slider-handle.png +0 -0
  182. bootstack/assets/widgets/slider-track-horizontal.png +0 -0
  183. bootstack/assets/widgets/slider-track-vertical.png +0 -0
  184. bootstack/assets/widgets/switch-off.png +0 -0
  185. bootstack/assets/widgets/switch-on.png +0 -0
  186. bootstack/assets/widgets/tabs-bar-horizontal.png +0 -0
  187. bootstack/assets/widgets/tabs-bar-vertical.png +0 -0
  188. bootstack/assets/widgets/tabs-pill.png +0 -0
  189. bootstack/cli/__init__.py +124 -0
  190. bootstack/cli/__main__.py +6 -0
  191. bootstack/cli/add.py +439 -0
  192. bootstack/cli/build.py +115 -0
  193. bootstack/cli/config.py +287 -0
  194. bootstack/cli/demo.py +1267 -0
  195. bootstack/cli/doctor.py +195 -0
  196. bootstack/cli/list_cmd.py +71 -0
  197. bootstack/cli/promote.py +120 -0
  198. bootstack/cli/pyinstaller.py +246 -0
  199. bootstack/cli/run.py +99 -0
  200. bootstack/cli/start.py +105 -0
  201. bootstack/cli/templates/__init__.py +861 -0
  202. bootstack/constants.py +325 -0
  203. bootstack/core/__init__.py +34 -0
  204. bootstack/core/capabilities/__init__.py +45 -0
  205. bootstack/core/capabilities/after.py +103 -0
  206. bootstack/core/capabilities/bind.py +154 -0
  207. bootstack/core/capabilities/bindtags.py +112 -0
  208. bootstack/core/capabilities/busy.py +61 -0
  209. bootstack/core/capabilities/clipboard.py +88 -0
  210. bootstack/core/capabilities/focus.py +118 -0
  211. bootstack/core/capabilities/grab.py +65 -0
  212. bootstack/core/capabilities/grid.py +188 -0
  213. bootstack/core/capabilities/localization.py +231 -0
  214. bootstack/core/capabilities/pack.py +119 -0
  215. bootstack/core/capabilities/place.py +92 -0
  216. bootstack/core/capabilities/selection.py +136 -0
  217. bootstack/core/capabilities/signals.py +242 -0
  218. bootstack/core/capabilities/winfo.py +315 -0
  219. bootstack/core/colorutils.py +234 -0
  220. bootstack/core/exceptions.py +95 -0
  221. bootstack/core/images.py +283 -0
  222. bootstack/core/localization/README.md +90 -0
  223. bootstack/core/localization/__init__.py +13 -0
  224. bootstack/core/localization/intl_format.py +580 -0
  225. bootstack/core/localization/msgcat.py +425 -0
  226. bootstack/core/localization/specs.py +143 -0
  227. bootstack/core/mixins/__init__.py +1 -0
  228. bootstack/core/mixins/ttk_state.py +35 -0
  229. bootstack/core/mixins/widget.py +132 -0
  230. bootstack/core/publisher.py +147 -0
  231. bootstack/core/signals/README.md +112 -0
  232. bootstack/core/signals/__init__.py +8 -0
  233. bootstack/core/signals/integration.py +100 -0
  234. bootstack/core/signals/signal.py +317 -0
  235. bootstack/core/signals/types.py +4 -0
  236. bootstack/core/validation/__init__.py +5 -0
  237. bootstack/core/validation/types.py +13 -0
  238. bootstack/core/validation/validation_result.py +17 -0
  239. bootstack/core/validation/validation_rules.py +112 -0
  240. bootstack/core/variables.py +62 -0
  241. bootstack/datasource/README.md +607 -0
  242. bootstack/datasource/__init__.py +51 -0
  243. bootstack/datasource/base.py +474 -0
  244. bootstack/datasource/file_source.py +541 -0
  245. bootstack/datasource/memory_source.py +482 -0
  246. bootstack/datasource/sqlite_source.py +453 -0
  247. bootstack/datasource/types.py +259 -0
  248. bootstack/dialogs/__init__.py +56 -0
  249. bootstack/dialogs/colorchooser.py +674 -0
  250. bootstack/dialogs/colordropper.py +257 -0
  251. bootstack/dialogs/datedialog.py +404 -0
  252. bootstack/dialogs/dialog.py +514 -0
  253. bootstack/dialogs/filterdialog.py +358 -0
  254. bootstack/dialogs/fontdialog.py +339 -0
  255. bootstack/dialogs/formdialog.py +541 -0
  256. bootstack/dialogs/message.py +489 -0
  257. bootstack/dialogs/query.py +561 -0
  258. bootstack/py.typed +1 -0
  259. bootstack/runtime/__init__.py +3 -0
  260. bootstack/runtime/app.py +879 -0
  261. bootstack/runtime/base_window.py +786 -0
  262. bootstack/runtime/events.py +399 -0
  263. bootstack/runtime/menu.py +510 -0
  264. bootstack/runtime/shortcuts.py +423 -0
  265. bootstack/runtime/tk_patch.py +31 -0
  266. bootstack/runtime/toplevel.py +131 -0
  267. bootstack/runtime/utility.py +371 -0
  268. bootstack/runtime/visual_focus.py +228 -0
  269. bootstack/runtime/window_utilities.py +1043 -0
  270. bootstack/style/__init__.py +5498 -0
  271. bootstack/style/bootstyle.py +507 -0
  272. bootstack/style/bootstyle_builder_base.py +752 -0
  273. bootstack/style/bootstyle_builder_mixed.py +93 -0
  274. bootstack/style/bootstyle_builder_tk.py +109 -0
  275. bootstack/style/bootstyle_builder_ttk.py +354 -0
  276. bootstack/style/builders/__init__.py +51 -0
  277. bootstack/style/builders/badge.py +44 -0
  278. bootstack/style/builders/button.py +453 -0
  279. bootstack/style/builders/buttongroup.py +344 -0
  280. bootstack/style/builders/calendar.py +271 -0
  281. bootstack/style/builders/checkbutton.py +95 -0
  282. bootstack/style/builders/combobox.py +112 -0
  283. bootstack/style/builders/contextmenu.py +268 -0
  284. bootstack/style/builders/entry.py +83 -0
  285. bootstack/style/builders/expander.py +171 -0
  286. bootstack/style/builders/field.py +312 -0
  287. bootstack/style/builders/frame.py +27 -0
  288. bootstack/style/builders/label.py +28 -0
  289. bootstack/style/builders/labelframe.py +41 -0
  290. bootstack/style/builders/listview.py +267 -0
  291. bootstack/style/builders/menubar.py +74 -0
  292. bootstack/style/builders/menubutton.py +408 -0
  293. bootstack/style/builders/notebook.py +316 -0
  294. bootstack/style/builders/panedwindow.py +25 -0
  295. bootstack/style/builders/progressbar.py +71 -0
  296. bootstack/style/builders/radiobutton.py +68 -0
  297. bootstack/style/builders/scale.py +66 -0
  298. bootstack/style/builders/scrollbar.py +360 -0
  299. bootstack/style/builders/separator.py +45 -0
  300. bootstack/style/builders/sidenav.py +313 -0
  301. bootstack/style/builders/sizegrip.py +15 -0
  302. bootstack/style/builders/spinbox.py +119 -0
  303. bootstack/style/builders/switch.py +67 -0
  304. bootstack/style/builders/tabitem.py +205 -0
  305. bootstack/style/builders/toolbutton.py +260 -0
  306. bootstack/style/builders/tooltip.py +26 -0
  307. bootstack/style/builders/treeview.py +269 -0
  308. bootstack/style/builders/utils.py +404 -0
  309. bootstack/style/builders_tk/__init__.py +16 -0
  310. bootstack/style/builders_tk/defaults.py +229 -0
  311. bootstack/style/element.py +173 -0
  312. bootstack/style/style.py +499 -0
  313. bootstack/style/theme_provider.py +449 -0
  314. bootstack/style/tk_patch.py +5 -0
  315. bootstack/style/token_maps.py +42 -0
  316. bootstack/style/types.py +32 -0
  317. bootstack/style/typography.py +527 -0
  318. bootstack/style/utility.py +696 -0
  319. bootstack/themes/__init__.py +12 -0
  320. bootstack/themes/standard.py +415 -0
  321. bootstack/themes/user.py +45 -0
  322. bootstack/widgets/__init__.py +53 -0
  323. bootstack/widgets/composites/__init__.py +38 -0
  324. bootstack/widgets/composites/accordion.py +385 -0
  325. bootstack/widgets/composites/appshell.py +445 -0
  326. bootstack/widgets/composites/buttongroup.py +391 -0
  327. bootstack/widgets/composites/calendar.py +914 -0
  328. bootstack/widgets/composites/compositeframe.py +282 -0
  329. bootstack/widgets/composites/contextmenu.py +1754 -0
  330. bootstack/widgets/composites/dateentry.py +261 -0
  331. bootstack/widgets/composites/dropdownbutton.py +190 -0
  332. bootstack/widgets/composites/expander.py +508 -0
  333. bootstack/widgets/composites/field.py +448 -0
  334. bootstack/widgets/composites/floodgauge.py +434 -0
  335. bootstack/widgets/composites/form.py +983 -0
  336. bootstack/widgets/composites/labeledscale.py +209 -0
  337. bootstack/widgets/composites/list/__init__.py +15 -0
  338. bootstack/widgets/composites/list/listitem.py +733 -0
  339. bootstack/widgets/composites/list/listview.py +1507 -0
  340. bootstack/widgets/composites/menubar.py +303 -0
  341. bootstack/widgets/composites/meter.py +882 -0
  342. bootstack/widgets/composites/numericentry.py +183 -0
  343. bootstack/widgets/composites/pagestack.py +330 -0
  344. bootstack/widgets/composites/passwordentry.py +149 -0
  345. bootstack/widgets/composites/pathentry.py +223 -0
  346. bootstack/widgets/composites/radiogroup.py +466 -0
  347. bootstack/widgets/composites/scrolledtext.py +388 -0
  348. bootstack/widgets/composites/scrolledtext.pyi +186 -0
  349. bootstack/widgets/composites/scrollview.py +675 -0
  350. bootstack/widgets/composites/selectbox.py +544 -0
  351. bootstack/widgets/composites/sidenav/__init__.py +24 -0
  352. bootstack/widgets/composites/sidenav/group.py +485 -0
  353. bootstack/widgets/composites/sidenav/header.py +83 -0
  354. bootstack/widgets/composites/sidenav/item.py +413 -0
  355. bootstack/widgets/composites/sidenav/separator.py +51 -0
  356. bootstack/widgets/composites/sidenav/view.py +919 -0
  357. bootstack/widgets/composites/spinnerentry.py +232 -0
  358. bootstack/widgets/composites/tableview/__init__.py +5 -0
  359. bootstack/widgets/composites/tableview/tableview.py +2254 -0
  360. bootstack/widgets/composites/tableview/types.py +169 -0
  361. bootstack/widgets/composites/tabs/__init__.py +6 -0
  362. bootstack/widgets/composites/tabs/tabitem.py +372 -0
  363. bootstack/widgets/composites/tabs/tabs.py +478 -0
  364. bootstack/widgets/composites/tabs/tabview.py +352 -0
  365. bootstack/widgets/composites/textentry.py +90 -0
  366. bootstack/widgets/composites/timeentry.py +189 -0
  367. bootstack/widgets/composites/toast.py +364 -0
  368. bootstack/widgets/composites/togglegroup.py +382 -0
  369. bootstack/widgets/composites/toolbar.py +393 -0
  370. bootstack/widgets/composites/tooltip.py +404 -0
  371. bootstack/widgets/internal/__init__.py +0 -0
  372. bootstack/widgets/internal/wrapper_base.py +304 -0
  373. bootstack/widgets/mixins/__init__.py +25 -0
  374. bootstack/widgets/mixins/configure_mixin.py +186 -0
  375. bootstack/widgets/mixins/entry_mixin.py +70 -0
  376. bootstack/widgets/mixins/font_mixin.py +346 -0
  377. bootstack/widgets/mixins/icon_mixin.py +38 -0
  378. bootstack/widgets/mixins/localization_mixin.py +255 -0
  379. bootstack/widgets/mixins/signal_mixin.py +272 -0
  380. bootstack/widgets/mixins/validation_mixin.py +204 -0
  381. bootstack/widgets/parts/__init__.py +11 -0
  382. bootstack/widgets/parts/numberentry_part.py +345 -0
  383. bootstack/widgets/parts/spinnerentry_part.py +394 -0
  384. bootstack/widgets/parts/textentry_part.py +344 -0
  385. bootstack/widgets/primitives/__init__.py +55 -0
  386. bootstack/widgets/primitives/badge.py +44 -0
  387. bootstack/widgets/primitives/button.py +89 -0
  388. bootstack/widgets/primitives/card.py +66 -0
  389. bootstack/widgets/primitives/checkbutton.py +124 -0
  390. bootstack/widgets/primitives/checktoggle.py +53 -0
  391. bootstack/widgets/primitives/combobox.py +165 -0
  392. bootstack/widgets/primitives/entry.py +98 -0
  393. bootstack/widgets/primitives/frame.py +206 -0
  394. bootstack/widgets/primitives/gridframe.py +479 -0
  395. bootstack/widgets/primitives/label.py +95 -0
  396. bootstack/widgets/primitives/labelframe.py +63 -0
  397. bootstack/widgets/primitives/menubutton.py +118 -0
  398. bootstack/widgets/primitives/notebook.py +551 -0
  399. bootstack/widgets/primitives/optionmenu.py +248 -0
  400. bootstack/widgets/primitives/packframe.py +228 -0
  401. bootstack/widgets/primitives/panedwindow.py +58 -0
  402. bootstack/widgets/primitives/progressbar.py +95 -0
  403. bootstack/widgets/primitives/radiobutton.py +115 -0
  404. bootstack/widgets/primitives/radiotoggle.py +50 -0
  405. bootstack/widgets/primitives/scale.py +85 -0
  406. bootstack/widgets/primitives/scrollbar.py +56 -0
  407. bootstack/widgets/primitives/separator.py +56 -0
  408. bootstack/widgets/primitives/sizegrip.py +47 -0
  409. bootstack/widgets/primitives/spinbox.py +91 -0
  410. bootstack/widgets/primitives/switch.py +41 -0
  411. bootstack/widgets/primitives/treeview.py +77 -0
  412. bootstack/widgets/types.py +20 -0
  413. bootstack-0.1.0a1.dist-info/METADATA +196 -0
  414. bootstack-0.1.0a1.dist-info/RECORD +419 -0
  415. bootstack-0.1.0a1.dist-info/WHEEL +5 -0
  416. bootstack-0.1.0a1.dist-info/entry_points.txt +2 -0
  417. bootstack-0.1.0a1.dist-info/licenses/LICENSE +22 -0
  418. bootstack-0.1.0a1.dist-info/licenses/NOTICE +10 -0
  419. bootstack-0.1.0a1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,752 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from collections.abc import Sequence
5
+ from typing import List, Optional, Tuple, Union
6
+
7
+ from typing_extensions import Any, TypedDict
8
+
9
+ from ttkbootstrap_icons_bs import BootstrapIcon
10
+ from ttkbootstrap_icons_bs.provider import BootstrapFontProvider
11
+ from ttkbootstrap_icons.providers import BaseFontProvider
12
+ from ttkbootstrap_icons.icon import Icon
13
+
14
+ # Custom y_bias for better vertical alignment in compound buttons.
15
+ # Default Bootstrap provider uses 0.02.
16
+ _ICON_Y_BIAS = 0.02
17
+ _icon_provider_initialized = False
18
+
19
+
20
+ class _TtkBootstrapIconProvider(BootstrapFontProvider):
21
+ """Custom Bootstrap icon provider with adjusted y_bias for bootstack.
22
+
23
+ The default Bootstrap provider uses y_bias=0.02, but bootstack buttons
24
+ need y_bias=0.08 for proper vertical alignment of icons with text.
25
+ """
26
+
27
+ def __init__(self):
28
+ # Bypass BootstrapFontProvider.__init__ and call BaseFontProvider directly
29
+ # to allow setting a custom y_bias value
30
+ BaseFontProvider.__init__(
31
+ self,
32
+ name="bootstrap",
33
+ display_name="Bootstrap Icons",
34
+ package="ttkbootstrap_icons_bs.assets",
35
+ homepage="https://icons.getbootstrap.com/",
36
+ license_url="https://github.com/twbs/icons/blob/main/LICENSE",
37
+ icon_version="1.13.1",
38
+ default_style="outline",
39
+ y_bias=_ICON_Y_BIAS,
40
+ styles={
41
+ "fill": {"filename": "bootstrap.ttf", "predicate": BootstrapFontProvider._is_fill_style},
42
+ "outline": {"filename": "bootstrap.ttf", "predicate": BootstrapFontProvider._is_outline_style},
43
+ }
44
+ )
45
+
46
+
47
+ def _ensure_icon_provider():
48
+ """Initialize the custom icon provider if not already done."""
49
+ global _icon_provider_initialized
50
+ if _icon_provider_initialized:
51
+ return
52
+ provider = _TtkBootstrapIconProvider()
53
+ Icon.initialize_with_provider(provider)
54
+ _icon_provider_initialized = True
55
+
56
+
57
+ from bootstack.style.theme_provider import ThemeProvider, use_theme
58
+ from bootstack.style.utility import best_foreground, color_to_hsl, darken_color, lighten_color, mix_colors, \
59
+ relative_luminance
60
+ from bootstack.runtime.utility import scale_size
61
+
62
+ # Source images are created at this resolution multiplier (e.g., 2x)
63
+ # This allows measurements from source images to be used directly in code
64
+ SOURCE_RESOLUTION = 2.0
65
+
66
+
67
+ class IconSpec(TypedDict, total=False):
68
+ name: str
69
+ size: Optional[int]
70
+ color: Optional[str]
71
+ state: Sequence[tuple[str, str | IconStateMap]]
72
+
73
+
74
+ class IconStateMap(TypedDict, total=False):
75
+ name: Optional[str]
76
+ color: Optional[str]
77
+
78
+
79
+ ForegroundStateSpec = tuple[str, str | dict[str, str]]
80
+
81
+
82
+ class BootstyleBuilderBase:
83
+ """Shared base for TTK and Tk bootstyle builders.
84
+
85
+ Centralizes theme provider plumbing and color utilities so both
86
+ BootstyleBuilder (TTK) and BootstyleBuilderTk (Tk) can inherit and
87
+ avoid duplication.
88
+ """
89
+
90
+ _RE_TOKEN = re.compile(r"^\s*(?P<head>[a-zA-Z_][\w-]*)(?P<brackets>(?:\[[^]]*])*)\s*$")
91
+
92
+ def __init__(self, theme_provider: ThemeProvider | None = None, style_instance: Any | None = None): # noqa: ANN401
93
+ # If no provider given, try to derive from style_instance
94
+ if theme_provider is None and style_instance is not None:
95
+ try:
96
+ theme_provider = style_instance.theme_provider # type: ignore[attr-defined]
97
+ except Exception:
98
+ theme_provider = None
99
+ self._provider = theme_provider or use_theme()
100
+ self._style = style_instance
101
+
102
+ def set_style_instance(self, style_instance: Any) -> None: # noqa: ANN401
103
+ self._style = style_instance
104
+
105
+ @property
106
+ def provider(self) -> ThemeProvider:
107
+ return self._provider
108
+
109
+ @property
110
+ def style(self) -> Any | None: # noqa: ANN401
111
+ return self._style
112
+
113
+ @property
114
+ def colors(self) -> dict:
115
+ return self.provider.colors
116
+
117
+ # ----- Color Utilities & Transformers -----
118
+
119
+ def _parse_color_token(self, token: str):
120
+ """Parse a color token into head and ordered list of modifiers.
121
+
122
+ Returns a dict with:
123
+ - head: The base color name (e.g., "primary")
124
+ - modifiers: List of (type, value) tuples in order
125
+ """
126
+ m = self._RE_TOKEN.match(token)
127
+ if not m:
128
+ return None
129
+
130
+ head = m.group("head")
131
+ brackets_raw = m.group("brackets")
132
+
133
+ modifiers = [] # List of (type, value) tuples in order
134
+
135
+ if brackets_raw:
136
+ # Extract all bracket contents: [content1][content2] -> ["content1", "content2"]
137
+ bracket_contents = re.findall(r'\[([^]]*)]', brackets_raw)
138
+
139
+ # Process each bracket in order for pipeline
140
+ for bracket in bracket_contents:
141
+ part = bracket.strip().lower()
142
+ if not part:
143
+ continue
144
+
145
+ if part.isdigit():
146
+ modifiers.append(("shade", int(part)))
147
+ elif re.fullmatch(r'[+-]\s*\d+', part):
148
+ rel = int(part.replace(" ", ""))
149
+ modifiers.append(("elevation", rel))
150
+ elif part == "subtle":
151
+ modifiers.append(("subtle", None))
152
+ elif part == "muted":
153
+ modifiers.append(("muted", None))
154
+
155
+ return {"head": head, "modifiers": modifiers}
156
+
157
+ def color(self, token: str, surface: str | None = None, role: str = "background") -> str:
158
+ """Resolve a color token with optional chained modifiers.
159
+
160
+ Modifiers are applied as a pipeline from left to right:
161
+ - primary[100][muted] → lookup primary[100], then apply muted
162
+ - background[+1][muted] → lookup background, elevate it, then apply muted
163
+
164
+ Args:
165
+ token: Color token (e.g., "primary", "primary[100][muted]")
166
+ Can also be a hex color string (e.g., "#ff0000")
167
+ surface: Optional surface color for subtle modifier
168
+ role: Role for subtle modifier ("background" or "text")
169
+
170
+ Returns:
171
+ The resolved color value as a hex string
172
+
173
+ Raises:
174
+ ValueError: If the color token cannot be resolved to a valid color.
175
+ """
176
+ # If token is already a hex color, return it directly
177
+ if token and token.startswith('#'):
178
+ return token
179
+
180
+ # Fast path: exact key (e.g., "blue[100]" or "primary")
181
+ direct = self.colors.get(token)
182
+ if direct is not None:
183
+ return direct
184
+
185
+ parsed = self._parse_color_token(token)
186
+ if not parsed:
187
+ result = self.colors.get(token)
188
+ if result is None:
189
+ raise ValueError(
190
+ f"Invalid color token: '{token}'. "
191
+ f"Valid color tokens include: primary, secondary, success, info, warning, danger, "
192
+ f"light, dark, background, foreground, or a hex color like '#ff0000'."
193
+ )
194
+ return result
195
+
196
+ head = parsed["head"]
197
+ modifiers = parsed["modifiers"]
198
+
199
+ # Determine base lookup key
200
+ # If first modifier is a shade, use it for lookup and remove from pipeline
201
+ base_key = head
202
+ if modifiers and modifiers[0][0] == "shade":
203
+ shade_value = modifiers[0][1]
204
+ base_key = f"{head}[{shade_value}]"
205
+ modifiers = modifiers[1:] # Remove shade from pipeline
206
+
207
+ # Get the base color
208
+ current_color = self.colors.get(base_key)
209
+ if current_color is None:
210
+ # Unknown base; fall back
211
+ result = self.colors.get(token)
212
+ if result is None:
213
+ raise ValueError(
214
+ f"Invalid color token: '{token}'. "
215
+ f"Valid color tokens include: primary, secondary, success, info, warning, danger, "
216
+ f"light, dark, background, foreground, or a hex color like '#ff0000'."
217
+ )
218
+ return result
219
+
220
+ # Apply each modifier in order as a pipeline transformation
221
+ for mod_type, mod_value in modifiers:
222
+ if mod_type == "elevation":
223
+ current_color = self.elevate(current_color, mod_value)
224
+ elif mod_type == "subtle":
225
+ # Subtle needs special handling - it does its own lookup
226
+ # Use the base_key for lookup, not the current transformed color
227
+ current_color = self.subtle(base_key, surface, role)
228
+ elif mod_type == "muted":
229
+ # Muted transforms whatever color we currently have
230
+ current_color = self.muted_foreground(current_color)
231
+
232
+ return current_color
233
+
234
+ def subtle(self, token: str, surface: str | None = None, role: str = "background") -> str:
235
+ """Return a subtle instance of this color for background or text."""
236
+ # Parse token to handle compound tokens like 'primary[subtle]' or 'primary[100]'
237
+ parsed = self._parse_color_token(token)
238
+ if parsed:
239
+ base_key = parsed["head"]
240
+ # If first modifier is a shade, include it in the lookup key
241
+ modifiers = parsed["modifiers"]
242
+ if modifiers and modifiers[0][0] == "shade":
243
+ base_key = f"{base_key}[{modifiers[0][1]}]"
244
+ base_color = self.colors.get(base_key)
245
+ else:
246
+ base_color = self.colors.get(token)
247
+
248
+ # Fallback if lookup failed
249
+ if base_color is None:
250
+ base_color = self.colors.get(token) or self.colors.get('foreground')
251
+
252
+ surface_val = surface or self.colors.get('background')
253
+
254
+ if role == "text":
255
+ if self.provider.mode == "light":
256
+ return darken_color(base_color, 0.25)
257
+ else:
258
+ return lighten_color(base_color, 0.25)
259
+ else: # background
260
+ if self.provider.mode == "light":
261
+ return mix_colors(base_color, surface_val, 0.08)
262
+ else:
263
+ return mix_colors(base_color, surface_val, 0.10)
264
+
265
+ def active(self, color: str) -> str:
266
+ return self._state_color(color, "active")
267
+
268
+ def pressed(self, color: str) -> str:
269
+ return self._state_color(color, "pressed")
270
+
271
+ def focus(self, color: str) -> str:
272
+ return self._state_color(color, "focus")
273
+
274
+ def selected(self, color: str) -> str:
275
+ return self._state_color(color, "selected")
276
+
277
+ def focus_border(self, color: str) -> str:
278
+ lum = relative_luminance(color)
279
+ if self.provider.mode == "dark":
280
+ return lighten_color(color, 0.1)
281
+ else:
282
+ return darken_color(color, 0.2 if lum > 0.5 else 0.1)
283
+
284
+ def focus_ring(self, color: str, surface: str | None = None) -> str:
285
+ surface = surface or self.color(color)
286
+ lum = relative_luminance(color)
287
+ if self.provider.mode == "dark":
288
+ if lum < 0.3:
289
+ brightened = lighten_color(color, 0.2)
290
+ mixed = mix_colors(brightened, surface, 0.2)
291
+ else:
292
+ mixed = mix_colors(color, surface, 0.3)
293
+ else:
294
+ if lum > 0.5:
295
+ blended = mix_colors(color, surface, 0.2)
296
+ mixed = darken_color(blended, 0.15)
297
+ else:
298
+ brightened = lighten_color(color, 0.25)
299
+ mixed = mix_colors(brightened, surface, 0.25)
300
+ return mixed
301
+
302
+ def focus_inner(self, fill: str) -> str:
303
+ """Internal focus line (2–3px), slightly brighter but not glowy."""
304
+ from bootstack.style.utility import contrast_ratio, hex_to_rgb
305
+
306
+ on = self.on_color(fill)
307
+
308
+ # Brighter defaults
309
+ w = 0.26 if self.provider.mode == "light" else 0.20
310
+ ring = mix_colors(on, fill, w)
311
+
312
+ # If still not distinct enough, bump once (bounded)
313
+ try:
314
+ bg = hex_to_rgb(fill)
315
+ fg = hex_to_rgb(ring)
316
+ if contrast_ratio(bg, fg) < 2.4:
317
+ w2 = min(w + 0.08, 0.34 if self.provider.mode == "light" else 0.28)
318
+ ring = mix_colors(on, fill, w2)
319
+ except Exception:
320
+ pass
321
+
322
+ return ring
323
+
324
+ def border(self, color: str, strength: float = 0.84) -> str:
325
+ """Derive a stroke color for a given surface by blending toward the surface's
326
+ computed on-color (text/icon color).
327
+ """
328
+ fg = self.on_color(color)
329
+ return mix_colors(color, fg, strength)
330
+
331
+ def on_color(self, color: str) -> str:
332
+ """Return a readable foreground color for the given background.
333
+
334
+ This is intentionally biased so that *dark-ish* accents get light
335
+ text rather than the mathematically highest-contrast dark text,
336
+ which tends to look wrong on buttons and pills.
337
+ """
338
+ background = self.color("background")
339
+ foreground = self.color("foreground")
340
+
341
+ try:
342
+ lum = relative_luminance(color)
343
+ except Exception:
344
+ candidates = [foreground, background, "#000000", "#ffffff"]
345
+ return best_foreground(color, candidates)
346
+
347
+ # Optional HSL-based accent detection to handle saturated mid-tone
348
+ # colors (like teal) that visually need light text even when their
349
+ # raw luminance is not very low.
350
+ hue = sat = hsl_lum = None
351
+ try:
352
+ hue, sat, hsl_lum = color_to_hsl(color, model="hex")
353
+ except Exception:
354
+ pass
355
+
356
+ if self.provider.mode == "light":
357
+ accent_force_light = False
358
+ if hue is not None and sat is not None and hsl_lum is not None:
359
+ # Treat saturated accents as needing light text when they are
360
+ # not extremely light overall. This catches teal/cyan/etc. but
361
+ # avoids triggering on near-white tinted backgrounds.
362
+ if sat >= 40 and hsl_lum <= 80:
363
+ # Strong teal/cyan/blue band (e.g., teal[600], cyan[600])
364
+ if 140 <= hue <= 220:
365
+ accent_force_light = True
366
+ # Other saturated accents, excluding the yellow/orange band,
367
+ # when not extremely light.
368
+ elif hsl_lum <= 70 and not (35 <= hue <= 70):
369
+ accent_force_light = True
370
+
371
+ # Saturated accents: force pure white text so contrast decisions
372
+ # don't accidentally favor dark foreground on mid-tone accents.
373
+ if accent_force_light:
374
+ candidates = ["#ffffff"]
375
+ # Anything darker than ~55% luminance is treated as a dark surface:
376
+ # always use light text, even if contrast math slightly prefers dark.
377
+ elif lum <= 0.55:
378
+ candidates = ["#ffffff"]
379
+ # Mid-light colors (e.g., warning/info) can work with either;
380
+ # allow contrast to choose, but still bias toward theme foreground.
381
+ elif lum <= 0.80:
382
+ candidates = [foreground, "#ffffff", "#000000"]
383
+ # Very light backgrounds -> dark text
384
+ else:
385
+ candidates = [foreground, "#000000"]
386
+ else:
387
+ # Dark mode: inverse bias.
388
+ if lum >= 0.75:
389
+ # Very light chips / badges -> dark text
390
+ candidates = ["#000000", foreground]
391
+ elif lum >= 0.45:
392
+ # Mid tones -> let contrast pick, but include light text
393
+ candidates = ["#ffffff", foreground, "#000000"]
394
+ else:
395
+ # Very dark surfaces -> light text
396
+ candidates = ["#ffffff", foreground]
397
+
398
+ # Deduplicate and remove empty candidates
399
+ unique: list[str] = []
400
+ for c in candidates:
401
+ if c and c not in unique:
402
+ unique.append(c)
403
+
404
+ return best_foreground(color, unique)
405
+
406
+ def muted_foreground(self, background: str, min_contrast: float = 4.5) -> str:
407
+ """Return a muted foreground color with adequate contrast.
408
+
409
+ Generates a subdued text color that maintains readability across
410
+ varying backgrounds by ensuring minimum WCAG contrast requirements.
411
+
412
+ Args:
413
+ background: The background color to contrast against.
414
+ min_contrast: Minimum WCAG contrast ratio (4.5 for AA, 7.0 for AAA).
415
+ Default is 4.5 (AA standard for normal text).
416
+
417
+ Returns:
418
+ A muted foreground color with adequate contrast against the background.
419
+ """
420
+ from bootstack.style.utility import hex_to_rgb, contrast_ratio
421
+
422
+ lum = relative_luminance(background)
423
+ bg_rgb = hex_to_rgb(background)
424
+
425
+ # Determine if we need light or dark muted text
426
+ if lum > 0.5:
427
+ # Light background -> use muted dark text
428
+ base_color = "#495057" # Dark gray
429
+ else:
430
+ # Dark background -> use muted light text
431
+ base_color = "#adb5bd" # Light gray
432
+
433
+ # Check if base muted color has adequate contrast
434
+ fg_rgb = hex_to_rgb(base_color)
435
+ ratio = contrast_ratio(bg_rgb, fg_rgb)
436
+
437
+ if ratio >= min_contrast:
438
+ return base_color
439
+
440
+ # If not enough contrast, adjust toward pure black/white
441
+ target = "#000000" if lum > 0.5 else "#ffffff"
442
+
443
+ # Binary search for minimum adjustment needed
444
+ for weight in [0.2, 0.4, 0.6, 0.8, 1.0]:
445
+ adjusted = mix_colors(target, base_color, weight)
446
+ adjusted_rgb = hex_to_rgb(adjusted)
447
+ ratio = contrast_ratio(bg_rgb, adjusted_rgb)
448
+ if ratio >= min_contrast:
449
+ return adjusted
450
+
451
+ # Fallback to pure contrast
452
+ return target
453
+
454
+ def disabled(self, role: str = "background", surface: str | None = None) -> str:
455
+ """Return a disabled color mixed with the surface.
456
+
457
+ Args:
458
+ role: 'background' for surfaces, 'text' for foregrounds.
459
+ surface: Optional surface color to mix against. If omitted,
460
+ uses the theme background.
461
+ """
462
+ surface = surface or self.color('background')
463
+
464
+ if role == "text":
465
+ if self.provider.mode == "light":
466
+ gray = "#6c757d"
467
+ mix_ratio = 0.35
468
+ else:
469
+ gray = "#adb5bd"
470
+ mix_ratio = 0.25
471
+ elif role == "background":
472
+ if self.provider.mode == "light":
473
+ gray = "#dee2e6"
474
+ mix_ratio = 0.15
475
+ else:
476
+ gray = "#495057"
477
+ mix_ratio = 0.20
478
+ else:
479
+ raise ValueError(f"Invalid role: {role}. Expected 'text' or 'background'.")
480
+
481
+ return mix_colors(gray, surface, mix_ratio)
482
+
483
+ def elevate(self, color: str, elevation: int = 0, max_elevation: int = 5) -> str:
484
+ if elevation <= 0:
485
+ return color
486
+ blend_target = "#000000" if self.provider.mode == "light" else "#ffffff"
487
+ weight = min(elevation / max_elevation, 1.0) * 0.3
488
+ return mix_colors(blend_target, color, weight)
489
+
490
+ @staticmethod
491
+ def _state_color(color: str, state: str) -> str:
492
+ if state == "focus":
493
+ return color
494
+
495
+ delta = {
496
+ "active": 0.08,
497
+ "selected": 0.18, # was effectively ~0.10–0.16; bump a bit
498
+ "pressed": 0.12,
499
+ "focus": 0.08,
500
+ }[state]
501
+
502
+ # Selected should read as "latched": always darken.
503
+ if state == "selected":
504
+ return darken_color(color, delta)
505
+
506
+ lum = relative_luminance(color)
507
+ if lum < 0.5:
508
+ return lighten_color(color, delta)
509
+ return darken_color(color, delta)
510
+
511
+ @staticmethod
512
+ def scale(value: Union[int, List, Tuple]):
513
+ return scale_size(value)
514
+
515
+ @staticmethod
516
+ def scale_from_source(value: Union[int, float, List, Tuple]):
517
+ """Scale a value measured in source image pixels.
518
+
519
+ Source images are created at SOURCE_RESOLUTION (2x). This method
520
+ converts source pixel measurements to 1x values, then applies DPI scaling.
521
+
522
+ Use this when you measure border, padding, or other values directly
523
+ from your source image assets.
524
+
525
+ Args:
526
+ value: Pixel measurement from source image (int, float, or sequence)
527
+
528
+ Returns:
529
+ DPI-scaled value appropriate for the current display
530
+
531
+ Example:
532
+ # If your source image has 10px corners:
533
+ border=b.scale_from_source(10) # Correctly scales for all DPIs
534
+ """
535
+ if isinstance(value, (int, float)):
536
+ return scale_size(int(value / SOURCE_RESOLUTION + 0.5))
537
+ elif isinstance(value, (tuple, list)):
538
+ scaled = [int(v / SOURCE_RESOLUTION + 0.5) for v in value]
539
+ return scale_size(scaled)
540
+ return scale_size(value)
541
+
542
+ # ----- Icon Utilities -----
543
+
544
+ @staticmethod
545
+ def normalize_icon_spec(icon: str | IconSpec, default_size: int = 18) -> IconSpec:
546
+ """
547
+ If the icon is a string, then create a icon spec where the name is known and the size is set to default_size.
548
+ If the there is an icon spec, map the default size if one is not already specified in the spec.
549
+
550
+ Icon sizes are automatically scaled based on DPI settings. The default size of 20px provides
551
+ a balanced appearance, being slightly larger than the visible text (16px) but not overwhelming.
552
+ """
553
+ from bootstack.runtime.utility import scale_size
554
+
555
+ # Apply DPI scaling to default size
556
+ scaled_default = scale_size(default_size)
557
+
558
+ if isinstance(icon, str):
559
+ return dict(name=icon, size=scaled_default)
560
+ else:
561
+ # Create a copy to avoid mutating the original dict
562
+ # This prevents size from growing on each theme change
563
+ result = icon.copy()
564
+ if 'size' not in result or result.get('size') is None:
565
+ result['size'] = scaled_default
566
+ elif result.get('size'):
567
+ # Scale user-provided size too
568
+ result['size'] = scale_size(result['size'])
569
+ return result
570
+
571
+ def map_stateful_icons(self, icon: IconSpec, foreground_spec: Sequence[tuple]):
572
+ """
573
+ Build and return a TTK image state map for icons, using the
574
+ configured icon provider.
575
+
576
+ Parameters:
577
+ icon (IconSpec):
578
+ Base icon spec with default `name` and optional `size`/`color`.
579
+ Optional `state` provides per-state overrides where the value
580
+ can be either a string (state-specific icon name) or a dict
581
+ with `name` and/or `color`.
582
+
583
+ foreground_spec (Sequence[tuple]):
584
+ A sequence of state → foreground color items computed by the
585
+ builder, e.g.:
586
+ [
587
+ ('disabled', '#fafafa'),
588
+ ('pressed !disabled', '#63f3f3'),
589
+ ('hover !disabled', '#f2fa5d'),
590
+ ]
591
+
592
+ Behavior:
593
+ - If `icon.color` is provided at the root, use it as the default
594
+ color for all states unless a per-state override is provided.
595
+ - If `icon.color` is not provided, the icon color follows the
596
+ widget's foreground color provided by `foreground_spec`, unless
597
+ a per-state override is provided in `icon.state`.
598
+ - If a per-state override provides `name`, use that icon name for
599
+ the state; otherwise, fallback to the base icon `name`.
600
+ - Identical (name, size, color) combinations reuse the same image.
601
+
602
+ Returns:
603
+ list[tuple[str, str]]: List of (state, image_name) tuples suitable
604
+ for `ttk.Style.element_create(..., 'image', default, (state, image) ...)`.
605
+ """
606
+ # Normalize base values
607
+ base_name: str = icon.get('name') # type: ignore[assignment]
608
+ if not base_name:
609
+ # Nothing we can do without an icon name
610
+ return []
611
+
612
+ base_size: int = int(icon.get('size') or 20)
613
+ base_color: str | None = icon.get('color')
614
+
615
+ # Build per-state override lookup: {state_str: {'name':..., 'color':...}}
616
+ state_overrides: dict[str, IconStateMap] = {}
617
+ for entry in icon.get('state', []) or []:
618
+ try:
619
+ st, ov = entry # type: ignore[misc]
620
+ except Exception:
621
+ continue
622
+ if isinstance(ov, str):
623
+ state_overrides[st] = {'name': ov}
624
+ elif isinstance(ov, dict):
625
+ # Only keep known keys
626
+ override: IconStateMap = {}
627
+ if 'name' in ov and ov['name']:
628
+ override['name'] = ov['name'] # type: ignore[assignment]
629
+ if 'color' in ov and ov['color']:
630
+ override['color'] = ov['color'] # type: ignore[assignment]
631
+ if override:
632
+ state_overrides[st] = override
633
+
634
+ # Obtain the provider callable (class or function). The provider itself
635
+ # is called as the icon constructor: provider(name, size, color) -> icon
636
+
637
+ def _resolve_fg(value: Any) -> str | None: # noqa: ANN401
638
+ """Extract a color string from the foreground state spec value."""
639
+ if isinstance(value, str):
640
+ return value
641
+ if isinstance(value, dict):
642
+ # Try common keys in order
643
+ for k in ('foreground', 'text', 'color'):
644
+ v = value.get(k)
645
+ if isinstance(v, str):
646
+ return v
647
+ return None
648
+
649
+ # Cache icons by (name, size, color) to avoid duplicates
650
+ cache: dict[tuple[str, int, str | None], Any] = {}
651
+
652
+ def _image_for(name: str, size: int, color: str | None):
653
+ key = (name, size, color)
654
+ if key in cache:
655
+ return cache[key]
656
+
657
+ # Special case: 'empty' creates a transparent placeholder image
658
+ if name == 'empty':
659
+ from bootstack.style.utility import create_transparent_image
660
+ img = create_transparent_image(size, size)
661
+ cache[key] = img
662
+ return img
663
+
664
+ # Ensure our custom icon provider is initialized with the correct y_bias
665
+ _ensure_icon_provider()
666
+
667
+ # Call the provider directly; it returns an icon object with `.image`
668
+ try:
669
+ icon_obj = BootstrapIcon(name=name, size=size, color=color) # type: ignore[misc]
670
+ except TypeError:
671
+ icon_obj = BootstrapIcon(name, size, color) # type: ignore[misc]
672
+ if icon_obj is None:
673
+ return None
674
+
675
+ cache[key] = icon_obj.image
676
+ return icon_obj.image
677
+
678
+ state_image_specs: list[tuple[str, Any]] = []
679
+
680
+ def _match_override(expr: str) -> IconStateMap | None:
681
+ # 1) Exact expression match (e.g., 'hover !disabled')
682
+ if expr in state_overrides:
683
+ return state_overrides[expr]
684
+ # 2) Token match: treat state keywords as tokens; ignore negations like '!disabled'
685
+ tokens = {t for t in expr.split() if t and not t.startswith('!')}
686
+ for key, ov in state_overrides.items():
687
+ if not key:
688
+ # skip empty-key overrides here; apply only to '' base state
689
+ continue
690
+ if key in tokens:
691
+ return ov
692
+ # 3) No match
693
+ return None
694
+
695
+ # Build ordered list of states to map
696
+ base_states: list[str] = [s for s, _ in foreground_spec]
697
+ fg_lookup: dict[str, Any] = {s: v for s, v in foreground_spec}
698
+
699
+ # Ensure typical derivations for overrides exist (e.g., 'hover' -> 'hover !disabled')
700
+ def _derive_expr(k: str) -> str:
701
+ if not k:
702
+ return ''
703
+ if ' ' in k or '!' in k:
704
+ return k
705
+ if k in ('pressed', 'active'):
706
+ return 'pressed !disabled'
707
+ if k == 'selected':
708
+ return 'selected !disabled'
709
+ if k == 'hover':
710
+ return 'hover !disabled'
711
+ return k
712
+
713
+ extra_states: list[str] = []
714
+ seen = set(base_states)
715
+ default_present = '' in seen
716
+ if default_present:
717
+ # Exclude default for ordering; add back later
718
+ base_states_no_default = [s for s in base_states if s != '']
719
+ else:
720
+ base_states_no_default = base_states[:]
721
+
722
+ # Add override-derived states if not already present
723
+ for k in state_overrides.keys():
724
+ expr = _derive_expr(k)
725
+ if expr not in seen:
726
+ extra_states.append(expr)
727
+ seen.add(expr)
728
+
729
+ # Compose final order: base (without ''), then extras, then default ''
730
+ ordered_states = base_states_no_default + extra_states
731
+ if default_present:
732
+ ordered_states.append('')
733
+
734
+ for state_expr in ordered_states:
735
+ fg_val = fg_lookup.get(state_expr)
736
+ # Derive per-state name/color
737
+ override = _match_override(state_expr) or {}
738
+ name = override.get('name', base_name) # type: ignore[assignment]
739
+ # Determine color priority: per-state override > base_color > foreground state color
740
+ color = override.get('color') # type: ignore[assignment]
741
+ if color is None:
742
+ if base_color is not None:
743
+ color = base_color
744
+ else:
745
+ color = _resolve_fg(fg_val)
746
+
747
+ # Build or reuse the image
748
+ img_or_photo = _image_for(name, base_size, color)
749
+ if img_or_photo is not None:
750
+ state_image_specs.append((state_expr, img_or_photo))
751
+
752
+ return state_image_specs