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,696 @@
1
+ from colorsys import hls_to_rgb, rgb_to_hls
2
+ from dataclasses import dataclass
3
+ from pathlib import Path
4
+ from typing import Tuple, Union, cast
5
+
6
+ from PIL import Image, ImageChops, ImageColor, ImageOps, ImageDraw
7
+ from PIL.ImageTk import PhotoImage
8
+
9
+ from bootstack.core.images import Image as ImageService
10
+ from bootstack.runtime.utility import clamp
11
+ from bootstack.style.types import ColorModel
12
+
13
+ ASSETS_DIR = Path(__file__).parent.parent / "assets" / "widgets"
14
+ ELEMENTS_DIR = Path(__file__).parent.parent / "assets" / "elements"
15
+
16
+
17
+ @dataclass
18
+ class ElementMeta:
19
+ """Scaled metadata for an element image.
20
+
21
+ All values are scaled for the current display DPI.
22
+ """
23
+ width: int
24
+ height: int
25
+ border: Union[int, Tuple[int, ...]]
26
+ padding: Union[int, Tuple[int, ...]]
27
+
28
+ def border_spec(self) -> Union[int, Tuple[int, ...]]:
29
+ """Return border in a format suitable for ttk element_create."""
30
+ return self.border
31
+
32
+ def padding_spec(self) -> Union[int, Tuple[int, ...]]:
33
+ """Return padding in a format suitable for ttk element_create."""
34
+ return self.padding
35
+
36
+
37
+ @dataclass
38
+ class ElementImageResult:
39
+ """Result of recolor_element_image containing the image and its metadata."""
40
+ image: PhotoImage
41
+ meta: ElementMeta
42
+
43
+
44
+ # Cached manifest data
45
+ _manifest_cache: dict | None = None
46
+
47
+
48
+ def _load_manifest() -> dict:
49
+ """Load and cache the element manifest."""
50
+ global _manifest_cache
51
+ if _manifest_cache is not None:
52
+ return _manifest_cache
53
+
54
+ try:
55
+ import tomllib
56
+ except ImportError:
57
+ import tomli as tomllib # type: ignore[import-not-found]
58
+
59
+ manifest_path = ELEMENTS_DIR / "manifest.toml"
60
+ with open(manifest_path, "rb") as f:
61
+ _manifest_cache = tomllib.load(f)
62
+
63
+ return _manifest_cache
64
+
65
+
66
+ def _get_element_info(key: str) -> dict | None:
67
+ """Get element info from manifest by key.
68
+
69
+ Args:
70
+ key: The element key (e.g., 'button_md', 'checkbox_checked')
71
+
72
+ Returns:
73
+ Dict with file, width, height, border, padding or None if not found.
74
+ """
75
+ manifest = _load_manifest()
76
+ images = manifest.get("images", {})
77
+ return images.get(key)
78
+
79
+ HUE = 360
80
+ SAT = 100
81
+ LUM = 100
82
+
83
+
84
+ def create_box_image(width: int, height: int, color: str):
85
+ cache_key = f"{width}{height}{color}"
86
+ cached = ImageService.get_cached(cache_key)
87
+ if cached is not None:
88
+ return cached
89
+
90
+ img = Image.new("RGBA", (width, height), color)
91
+ pm = PhotoImage(image=img)
92
+
93
+ ImageService.set_cached(cache_key, pm)
94
+ return pm
95
+
96
+
97
+ def create_rounded_border_image(*,
98
+ size=200,
99
+ radius=4,
100
+ thickness=2,
101
+ fill='#ffffff',
102
+ stroke='#000000'
103
+ ):
104
+ cache_key = f"{size}{radius}{thickness}{fill}{stroke}"
105
+ cached = ImageService.get_cached(cache_key)
106
+ if cached is not None:
107
+ return cached
108
+
109
+ scale = 2 # light supersample for corner smoothing
110
+
111
+ s = size * scale
112
+ r = radius * scale
113
+ t = thickness * scale
114
+
115
+ img = Image.new("RGBA", (s, s), (0, 0, 0, 0))
116
+ d = ImageDraw.Draw(img)
117
+
118
+ # Two-rectangle approach for precise bounds control:
119
+ # 1. Outer rounded rect (stroke color) - fills to edges
120
+ # 2. Inner rounded rect (fill color) - inset by stroke thickness
121
+ outer_radius = r + t
122
+ d.rounded_rectangle(
123
+ (0, 0, s - 1, s - 1),
124
+ radius=outer_radius,
125
+ fill=stroke,
126
+ )
127
+
128
+ # Inner rect inset by stroke thickness
129
+ d.rounded_rectangle(
130
+ (t, t, s - 1 - t, s - 1 - t),
131
+ radius=r,
132
+ fill=fill,
133
+ )
134
+
135
+ # Resize with LANCZOS for smooth corners, then sharpen to restore edge crispness
136
+ from PIL import ImageFilter
137
+ img = img.resize((size, size), Image.Resampling.LANCZOS)
138
+ img = img.filter(ImageFilter.UnsharpMask(radius=0.5, percent=150, threshold=0))
139
+
140
+ pm = PhotoImage(image=img)
141
+ ImageService.set_cached(cache_key, pm)
142
+ return pm
143
+
144
+
145
+ def create_transparent_image(width: int, height: int):
146
+ """Create a transparent image."""
147
+ cache_key = f"transparent_{width}x{height}"
148
+ cached = ImageService.get_cached(cache_key)
149
+ if cached is not None:
150
+ return cached
151
+
152
+ img = Image.new('RGBA', (width, height), (255, 255, 255, 0))
153
+ pm = PhotoImage(image=img)
154
+ ImageService.set_cached(cache_key, pm)
155
+ return pm
156
+
157
+
158
+ def color_to_rgb(color, model: ColorModel = 'hex'):
159
+ """Convert color value to rgb.
160
+
161
+ The color and model parameters represent the color to be converted.
162
+ The value is expected to be a string for "name" and "hex" models and
163
+ a Tuple or List for "rgb" and "hsl" models.
164
+
165
+ **Parameters**
166
+
167
+ - `color` (Any): The color values for the model being converted.
168
+ - `model` (Literal['rbg', 'hsl', 'hex']): The color model being converted.
169
+
170
+ **Returns**
171
+
172
+ `Tuple[int, int, int]` — The rgb color values.
173
+ """
174
+ conformed = conform_color_model(color, model)
175
+ return ImageColor.getrgb(conformed)
176
+
177
+
178
+ def color_to_hex(color, model: ColorModel = 'rgb'):
179
+ """Convert color value to hex.
180
+
181
+ The color and model parameters represent the color to be converted.
182
+ The value is expected to be a string for "name" and "hex" models and
183
+ a Tuple or List for "rgb" and "hsl" models.
184
+
185
+ **Parameters**
186
+
187
+ - `color` (Any): The color values for the model being converted.
188
+ - `model` (Literal['rgb', 'hsl', 'hex']): The color model being converted.
189
+
190
+ **Returns**
191
+
192
+ `str` — The hexadecimal color value.
193
+ """
194
+ r, g, b = color_to_rgb(color, model)
195
+ return f'#{r:02x}{g:02x}{b:02x}'
196
+
197
+
198
+ def color_to_hsl(color, model: ColorModel = 'hex'):
199
+ """Convert color value to hsl.
200
+
201
+ The color and model parameters represent the color to be converted.
202
+ The value is expected to be a string for "name" and "hex" models and
203
+ a Tuple or List for "rgb" and "hsl" models.
204
+
205
+ **Parameters**
206
+
207
+ - `color` (Any): The color values for the model being converted.
208
+ - `model` (Literal['rgb', 'hsl', 'hex']): The color model being converted.
209
+
210
+ **Returns**
211
+
212
+ `Tuple[int, int, int]` — The hsl color values.
213
+ """
214
+ r, g, b = color_to_rgb(color, model)
215
+ hls = rgb_to_hls(r / 255, g / 255, b / 255)
216
+ hue = int(clamp(hls[0] * HUE, 0, HUE))
217
+ lum = int(clamp(hls[1] * LUM, 0, LUM))
218
+ sat = int(clamp(hls[2] * SAT, 0, SAT))
219
+ return hue, sat, lum
220
+
221
+
222
+ def update_hsl_value(
223
+ color, hue=None, sat=None, lum=None,
224
+ in_model: ColorModel = 'hsl',
225
+ out_model: ColorModel = 'hsl'):
226
+ """Change hue, saturation, or luminosity of the color based on the hue,
227
+ sat, lum parameters provided.
228
+
229
+ **Parameters**
230
+
231
+ - `color` (Any): The color.
232
+ - `hue` (int, optional): A number between 0 and 360.
233
+ - `sat` (int, optional): A number between 0 and 100.
234
+ - `lum` (int, optional): A number between 0 and 100.
235
+ - `in_model` (Literal['rgb', 'hsl', 'hex']): The color model of the input color.
236
+ - `out_model` (Literal['rgb', 'hsl', 'hex']): The color model of the output color.
237
+
238
+ **Returns**
239
+
240
+ `Tuple[int, int, int]` — The color value based on the selected color model.
241
+ """
242
+ h, s, l = color_to_hsl(color, in_model)
243
+ if hue is not None:
244
+ h = hue
245
+ if sat is not None:
246
+ s = sat
247
+ if lum is not None:
248
+ l = lum
249
+ if out_model == 'rgb':
250
+ return color_to_rgb([h, s, l], 'hsl')
251
+ elif out_model == 'hex':
252
+ return color_to_hex([h, s, l], 'hsl')
253
+ else:
254
+ return h, s, l
255
+
256
+
257
+ def contrast_color(
258
+ color, model: ColorModel, dark_color='#000',
259
+ light_color='#fff'):
260
+ """The best matching contrasting light or dark color for the given color.
261
+ https://stackoverflow.com/questions/1855884/determine-font-color-based-on-background-color
262
+
263
+ **Parameters**
264
+
265
+ - `color` (str): The color value to evaluate.
266
+ - `model` (Literal['rgb', 'hsl', 'hex']): The model of the color value to be evaluated. 'rgb' by default.
267
+ - `dark_color` (str): The color of the dark contrasting color.
268
+ - `light_color` (str): The color of the light contrasting color.
269
+
270
+ **Returns**
271
+
272
+ `str` — The matching color value.
273
+ """
274
+ if model != 'rgb':
275
+ r, g, b = color_to_rgb(color, model)
276
+ else:
277
+ r, g, b = color
278
+
279
+ luminance = ((0.299 * r) + (0.587 * g) + (0.114 * b)) / 255
280
+ if luminance > 0.5:
281
+ return dark_color
282
+ else:
283
+ return light_color
284
+
285
+
286
+ def conform_color_model(color, model: ColorModel):
287
+ """Conform the color values to a string that can be interpreted by the
288
+ `PIL.ImageColor.getrgb method`.
289
+
290
+ **Parameters**
291
+
292
+ - `color` (Any): The color value to conform.
293
+ - `model` (Literal['rgb', 'hsl', 'hex']): The model of the color to evaluate (rgb, hex, hsl).
294
+
295
+ **Returns**
296
+
297
+ `str` — A color value string that can be used as a parameter in the PIL.ImageColor.getrgb method.
298
+ """
299
+ if model == 'hsl':
300
+ hue = clamp(color[0], 0, HUE)
301
+ sat = clamp(color[1], 0, SAT)
302
+ lum = clamp(color[2], 0, LUM)
303
+ return f'hsl({hue},{sat}%,{lum}%)'
304
+ elif model == 'rgb':
305
+ red = clamp(color[0], 0, 255)
306
+ grn = clamp(color[1], 0, 255)
307
+ blu = clamp(color[2], 0, 255)
308
+ return f'rgb({red},{grn},{blu})'
309
+ else:
310
+ return color
311
+
312
+
313
+ def make_transparent(alpha, foreground, background='#fff'):
314
+ """Simulate color transparency.
315
+
316
+ **Parameters**
317
+
318
+ - `alpha` (float): The amount of transparency between 0.0 and 1.0.
319
+ - `foreground` (str): The foreground color.
320
+ - `background` (str): The background color.
321
+
322
+ **Returns**
323
+
324
+ `str` — A hexadecimal color representing the "transparent" version of the foreground color against the background color.
325
+ """
326
+ fg = ImageColor.getrgb(foreground)
327
+ bg = ImageColor.getrgb(background)
328
+ rgb_float = [alpha * c1 + (1 - alpha) * c2 for (c1, c2) in zip(fg, bg)]
329
+ rgb_int = [int(x) for x in rgb_float]
330
+ return '#{:02x}{:02x}{:02x}'.format(*rgb_int)
331
+
332
+
333
+ def open_image(name: str) -> Image.Image:
334
+ """
335
+ Load a white-layout image from file.
336
+
337
+ Args:
338
+ name: The asset name without extension (e.g. "add" loads "add.png")
339
+
340
+ Returns:
341
+ A Pillow RGBA Image object.
342
+ """
343
+ path = ASSETS_DIR / f"{name}.png"
344
+ return Image.open(path).convert("RGBA")
345
+
346
+
347
+ def _open_element_image(filename: str) -> Image.Image:
348
+ """Load an element image from the elements directory.
349
+
350
+ Args:
351
+ filename: The filename (e.g., "button-lg.png")
352
+
353
+ Returns:
354
+ A Pillow RGBA Image object.
355
+ """
356
+ path = ELEMENTS_DIR / filename
357
+ return Image.open(path).convert("RGBA")
358
+
359
+
360
+ def _scale_border_or_padding(
361
+ value: int | list | tuple,
362
+ scale: float
363
+ ) -> int | tuple[int, ...]:
364
+ """Scale border or padding values.
365
+
366
+ Args:
367
+ value: Integer or sequence of integers from manifest
368
+ scale: Scale factor to apply
369
+
370
+ Returns:
371
+ Scaled value(s) as int or tuple
372
+ """
373
+ if isinstance(value, int):
374
+ return max(1, int(value * scale + 0.5)) if value > 0 else 0
375
+ elif isinstance(value, (list, tuple)):
376
+ scaled = tuple(
377
+ max(1, int(v * scale + 0.5)) if v > 0 else 0
378
+ for v in value
379
+ )
380
+ return scaled
381
+ return value
382
+
383
+
384
+ def recolor_element_image(
385
+ key: str,
386
+ white_color: str,
387
+ black_color: str = "#ffffff",
388
+ magenta_color: str | None = None,
389
+ transparent_color: str | None = None,
390
+ ) -> ElementImageResult:
391
+ """Recolor an element image from the manifest and return scaled image with metadata.
392
+
393
+ This function fetches an image by key from the elements manifest, applies
394
+ luminance-based recoloring, and returns both the scaled image and scaled
395
+ metadata (border, padding, dimensions) for use with ttk element_create.
396
+
397
+ The scaling is cross-platform aware, handling the differences between
398
+ logical and physical pixels on macOS, Windows, and Linux:
399
+ - Windows: Uses system DPI scaling (96 DPI baseline, scales to 120, 144, etc.)
400
+ - macOS: Retina displays use 2x physical pixels per logical pixel
401
+ - Linux: Uses X11/Wayland DPI settings via winfo_fpixels
402
+
403
+ Source images are created at 2x resolution (default_dpi=2.0 in manifest),
404
+ so scaling accounts for this to produce correctly sized output.
405
+
406
+ Args:
407
+ key: Element key from manifest (e.g., 'button_md', 'checkbox_checked')
408
+ white_color: Replace white/light areas with this color (hex string)
409
+ black_color: Replace black/dark areas with this color (hex string)
410
+ magenta_color: Replace magenta (#ff00ff) with this color, if provided
411
+ transparent_color: Fill fully transparent areas with this color
412
+
413
+ Returns:
414
+ ElementImageResult containing:
415
+ - image: The recolored and scaled PhotoImage
416
+ - meta: ElementMeta with scaled width, height, border, and padding
417
+
418
+ Raises:
419
+ ValueError: If the key is not found in the manifest.
420
+
421
+ Example:
422
+ >>> result = recolor_element_image('button_md', '#3b82f6', '#1e40af')
423
+ >>> element = ElementImage(
424
+ ... 'MyButton.border',
425
+ ... result.image,
426
+ ... border=result.meta.border,
427
+ ... padding=result.meta.padding
428
+ ... )
429
+ """
430
+ # Get element info from manifest
431
+ info = _get_element_info(key)
432
+ if info is None:
433
+ raise ValueError(
434
+ f"Element key '{key}' not found in manifest. "
435
+ f"Check that the key exists in assets/elements/manifest.toml"
436
+ )
437
+
438
+ # Get source resolution from manifest
439
+ manifest = _load_manifest()
440
+ source_resolution = manifest.get("default_dpi", 2.0)
441
+
442
+ # Calculate cross-platform scale factor
443
+ from bootstack.runtime.utility import _ScalingState
444
+ scale = _ScalingState.get_image_scale(source_resolution=source_resolution)
445
+
446
+ # Create cache key from all parameters
447
+ cache_key = (
448
+ "element", key, white_color, black_color,
449
+ magenta_color, transparent_color, scale
450
+ )
451
+
452
+ # Check if we've already created this exact result
453
+ cached = ImageService.get_cached(cache_key)
454
+ if cached is not None:
455
+ # Cached value is a tuple of (PhotoImage, ElementMeta)
456
+ return ElementImageResult(image=cached[0], meta=cached[1])
457
+
458
+ # Load and process the image
459
+ filename = info.get("file", f"{key.replace('_', '-')}.png")
460
+ img = _open_element_image(filename)
461
+ gray = ImageOps.grayscale(img)
462
+
463
+ fg_rgb = color_to_rgb(white_color)
464
+ bg_rgb = color_to_rgb(black_color)
465
+ mag_rgb = color_to_rgb(magenta_color) if magenta_color else None
466
+ trans_rgb = color_to_rgb(transparent_color) if transparent_color else None
467
+
468
+ # Luminance-based recoloring via per-channel LUTs applied to grayscale.
469
+ # Output channel value = round(bg + (fg - bg) * gray / 255). PIL applies
470
+ # the LUT in C, replacing the per-pixel Python loop.
471
+ def _channel_lut(bg_c: int, fg_c: int) -> list[int]:
472
+ return [round(bg_c + (fg_c - bg_c) * i / 255.0) for i in range(256)]
473
+
474
+ r_chan = gray.point(_channel_lut(bg_rgb[0], fg_rgb[0]))
475
+ g_chan = gray.point(_channel_lut(bg_rgb[1], fg_rgb[1]))
476
+ b_chan = gray.point(_channel_lut(bg_rgb[2], fg_rgb[2]))
477
+
478
+ r_src, g_src, b_src, alpha = img.split()
479
+ result = Image.merge("RGBA", (r_chan, g_chan, b_chan, alpha))
480
+
481
+ # Magenta passthrough: pixels whose source RGB is exactly (255, 0, 255)
482
+ # are replaced with mag_rgb. Build a 0/255 mask via channel LUTs and
483
+ # AND them with ImageChops.multiply (255 * 255 / 255 = 255).
484
+ if mag_rgb:
485
+ r_eq = r_src.point([255 if i == 255 else 0 for i in range(256)])
486
+ g_eq = g_src.point([255 if i == 0 else 0 for i in range(256)])
487
+ b_eq = b_src.point([255 if i == 255 else 0 for i in range(256)])
488
+ mag_mask = ImageChops.multiply(ImageChops.multiply(r_eq, g_eq), b_eq)
489
+ mag_solid = Image.new("RGBA", img.size, (*mag_rgb, 255))
490
+ composited = Image.composite(mag_solid, result, mag_mask)
491
+ # composite replaces alpha too; restore the original alpha.
492
+ nr, ng, nb, _ = composited.split()
493
+ result = Image.merge("RGBA", (nr, ng, nb, alpha))
494
+
495
+ # Flatten partial transparency over a solid backing color. Matches the
496
+ # old per-pixel `r_final = trans*(1-a) + r*a, alpha=255` exactly.
497
+ if trans_rgb:
498
+ backing = Image.new("RGBA", img.size, (*trans_rgb, 255))
499
+ result = Image.alpha_composite(backing, result)
500
+
501
+ # Scale the image
502
+ # Use rounding (+ 0.5) to ensure image size matches metadata dimensions exactly.
503
+ # This prevents centering issues caused by mismatched image/border calculations.
504
+ if scale != 1.0:
505
+ new_size = (
506
+ max(1, int(result.width * scale + 0.5)),
507
+ max(1, int(result.height * scale + 0.5))
508
+ )
509
+ # Use NEAREST for sharp-edge assets (avoids antialiasing at color boundaries)
510
+ # Use LANCZOS for smooth assets (rounded corners, gradients)
511
+ resample_mode = info.get("resample", "lanczos").lower()
512
+ if resample_mode == "nearest":
513
+ result = result.resize(new_size, Image.Resampling.NEAREST)
514
+ else:
515
+ result = result.resize(new_size, Image.Resampling.LANCZOS)
516
+
517
+ photo_image = PhotoImage(image=result)
518
+
519
+ # Scale the metadata
520
+ source_width = info.get("width", img.width)
521
+ source_height = info.get("height", img.height)
522
+ source_border = info.get("border", 0)
523
+ source_padding = info.get("padding", 0)
524
+
525
+ meta = ElementMeta(
526
+ width=max(1, int(source_width * scale + 0.5)),
527
+ height=max(1, int(source_height * scale + 0.5)),
528
+ border=_scale_border_or_padding(source_border, scale),
529
+ padding=_scale_border_or_padding(source_padding, scale),
530
+ )
531
+
532
+ # Cache the result as a tuple
533
+ ImageService.set_cached(cache_key, (photo_image, meta))
534
+
535
+ return ElementImageResult(image=photo_image, meta=meta)
536
+
537
+
538
+ def should_darken(bg_hex: str) -> bool:
539
+ """Determine whether to darken or lighten based on luminance and saturation."""
540
+ r, g, b = [v / 255.0 for v in ImageColor.getrgb(bg_hex)]
541
+ h, l, s = rgb_to_hls(r, g, b)
542
+
543
+ # If the color is light and saturated, lighten it (like warning/info)
544
+ # If the color is already very light (lightness > 0.9), darken it to gain contrast
545
+ if l > 0.8:
546
+ return True # Darken very light colors (like `light`)
547
+ if l < 0.3:
548
+ return False # Lighten very dark colors (like `dark`)
549
+ if s > 0.6 and l > 0.6:
550
+ return False # Lighten vibrant, light colors (e.g. warning/info)
551
+ return True # Default: darken
552
+
553
+
554
+ def darken_color(hex_color: str, percent: float) -> str:
555
+ """Darken a hex color by reducing lightness in HLS color space."""
556
+ r, g, b = [v / 255.0 for v in ImageColor.getrgb(hex_color)]
557
+ h, l, s = rgb_to_hls(r, g, b)
558
+ l = max(0.0, l * (1 - percent))
559
+ r, g, b = hls_to_rgb(h, l, s)
560
+ return f"#{int(r * 255):02x}{int(g * 255):02x}{int(b * 255):02x}"
561
+
562
+
563
+ def lighten_color(hex_color: str, percent: float) -> str:
564
+ """Lighten a hex color by increasing lightness in HLS color space."""
565
+ r, g, b = [v / 255.0 for v in ImageColor.getrgb(hex_color)]
566
+ h, l, s = rgb_to_hls(r, g, b)
567
+ l = min(1.0, l + (1 - l) * percent)
568
+ r, g, b = hls_to_rgb(h, l, s)
569
+ return f"#{int(r * 255):02x}{int(g * 255):02x}{int(b * 255):02x}"
570
+
571
+
572
+ def mix_colors(color1: str, color2: str, weight: float) -> str:
573
+ """Mix two colors by weight.
574
+
575
+ Args:
576
+ color1: The foreground color in hex format (e.g., '#FF0000').
577
+ color2: The background color in hex format.
578
+ weight: A float from 0 to 1, where 1 favors color1 and 0 favors color2.
579
+
580
+ Returns:
581
+ A hex color string representing the mixed result.
582
+ """
583
+ r1, g1, b1 = ImageColor.getrgb(color1)
584
+ r2, g2, b2 = ImageColor.getrgb(color2)
585
+
586
+ r = round(r1 * weight + r2 * (1 - weight))
587
+ g = round(g1 * weight + g2 * (1 - weight))
588
+ b = round(b1 * weight + b2 * (1 - weight))
589
+
590
+ return f"#{r:02X}{g:02X}{b:02X}"
591
+
592
+
593
+ def tint_color(color: str, base_weight: float) -> str:
594
+ """Tint a color by mixing it with white.
595
+
596
+ Args:
597
+ color: The base color in hex.
598
+ base_weight: Amount of base color to retain (0–1).
599
+
600
+ Returns:
601
+ A tinted hex color string.
602
+ """
603
+ return mix_colors(color, "#ffffff", 1 - base_weight)
604
+
605
+
606
+ def shade_color(color: str, base_weight: float) -> str:
607
+ """Shade a color by mixing it with black.
608
+
609
+ Args:
610
+ color: The base color in hex.
611
+ base_weight: Amount of base color to retain (0–1).
612
+
613
+ Returns:
614
+ A shaded hex color string.
615
+ """
616
+ return mix_colors(color, "#000000", 1 - base_weight)
617
+
618
+
619
+ def relative_luminance(hex_color: str) -> float:
620
+ """Calculate the relative luminance of a color.
621
+
622
+ Args:
623
+ hex_color: A hex color string.
624
+
625
+ Returns:
626
+ The luminance value from 0 (black) to 1 (white).
627
+ """
628
+ r, g, b = [x / 255 for x in ImageColor.getrgb(hex_color)]
629
+
630
+ def adjust(c):
631
+ return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4
632
+
633
+ r, g, b = adjust(r), adjust(g), adjust(b)
634
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b
635
+
636
+
637
+ def contrast_ratio(rgb1: tuple[int, int, int], rgb2: tuple[int, int, int]) -> float:
638
+ def rel_luminance(r: int, g: int, b: int) -> float:
639
+ def channel(c): return (c / 255.0) ** 2.2
640
+
641
+ return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b)
642
+
643
+ lum1 = rel_luminance(*rgb1)
644
+ lum2 = rel_luminance(*rgb2)
645
+ lighter = max(lum1, lum2)
646
+ darker = min(lum1, lum2)
647
+ return (lighter + 0.05) / (darker + 0.05)
648
+
649
+
650
+ def hex_to_rgb(value: str) -> tuple[int, int, int]:
651
+ """Convert a hex color string to an RGB tuple."""
652
+ value = value.lstrip("#")
653
+ lv = len(value)
654
+ result = tuple(int(value[i:i + lv // 3], 16) for i in range(0, lv, lv // 3))
655
+ return cast(tuple[int, int, int], result)
656
+
657
+
658
+ def rgb_to_hex(rgb: tuple[int, int, int]) -> str:
659
+ """Convert an RGB tuple to a hex color string."""
660
+ return "#{:02x}{:02x}{:02x}".format(*rgb)
661
+
662
+
663
+ def hsl_to_hex(h: float, s: float, l: float) -> str:
664
+ """Convert HSL values to a hex color string.
665
+
666
+ Args:
667
+ h: Hue in degrees (0-360)
668
+ s: Saturation as percentage (0-100)
669
+ l: Lightness as percentage (0-100)
670
+
671
+ Returns:
672
+ Hex color string (e.g., '#ff0000')
673
+ """
674
+ # Normalize to 0-1 range
675
+ h_norm = h / 360.0
676
+ s_norm = s / 100.0
677
+ l_norm = l / 100.0
678
+
679
+ # colorsys uses HLS order (not HSL)
680
+ r, g, b = hls_to_rgb(h_norm, l_norm, s_norm)
681
+
682
+ return rgb_to_hex((round(r * 255), round(g * 255), round(b * 255)))
683
+
684
+
685
+ def best_foreground(bg_color: str, candidates: list[str] = None) -> str:
686
+ """Return the color with the highest contrast against the background."""
687
+ if candidates is None:
688
+ candidates = ["#000000", "#ffffff"]
689
+
690
+ bg_rgb = hex_to_rgb(bg_color)
691
+
692
+ def contrast(c: str) -> float:
693
+ fg_rgb = hex_to_rgb(c)
694
+ return contrast_ratio(bg_rgb, fg_rgb)
695
+
696
+ return max(candidates, key=contrast)
@@ -0,0 +1,12 @@
1
+ """Theme definitions for bootstack.
2
+
3
+ This package contains theme definitions including standard built-in themes
4
+ and user-defined custom themes.
5
+
6
+ Modules:
7
+ standard: Built-in Bootstrap-inspired theme definitions
8
+ user: User-defined custom theme storage
9
+
10
+ The themes are defined as dictionaries containing color schemes and type
11
+ information (light/dark) used by the Style class to create themed widgets.
12
+ """