bootstack 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (471) hide show
  1. bootstack/__init__.py +157 -0
  2. bootstack/__main__.py +5 -0
  3. bootstack/_core/__init__.py +21 -0
  4. bootstack/_core/capabilities/__init__.py +45 -0
  5. bootstack/_core/capabilities/after.py +103 -0
  6. bootstack/_core/capabilities/bind.py +192 -0
  7. bootstack/_core/capabilities/bindtags.py +112 -0
  8. bootstack/_core/capabilities/busy.py +72 -0
  9. bootstack/_core/capabilities/clipboard.py +89 -0
  10. bootstack/_core/capabilities/focus.py +118 -0
  11. bootstack/_core/capabilities/grab.py +65 -0
  12. bootstack/_core/capabilities/grid.py +211 -0
  13. bootstack/_core/capabilities/localization.py +231 -0
  14. bootstack/_core/capabilities/pack.py +140 -0
  15. bootstack/_core/capabilities/place.py +113 -0
  16. bootstack/_core/capabilities/selection.py +136 -0
  17. bootstack/_core/capabilities/signals.py +244 -0
  18. bootstack/_core/capabilities/winfo.py +315 -0
  19. bootstack/_core/colorutils.py +234 -0
  20. bootstack/_core/exceptions.py +25 -0
  21. bootstack/_core/images.py +463 -0
  22. bootstack/_core/mixins/__init__.py +1 -0
  23. bootstack/_core/mixins/ttk_state.py +35 -0
  24. bootstack/_core/mixins/widget.py +132 -0
  25. bootstack/_core/paths.py +49 -0
  26. bootstack/_core/publisher.py +149 -0
  27. bootstack/_core/variables.py +62 -0
  28. bootstack/_runtime/__init__.py +3 -0
  29. bootstack/_runtime/app.py +930 -0
  30. bootstack/_runtime/base_window.py +945 -0
  31. bootstack/_runtime/events.py +399 -0
  32. bootstack/_runtime/shortcuts.py +496 -0
  33. bootstack/_runtime/tk_patch.py +43 -0
  34. bootstack/_runtime/toplevel.py +276 -0
  35. bootstack/_runtime/utility.py +457 -0
  36. bootstack/_runtime/visual_focus.py +240 -0
  37. bootstack/_runtime/window_utilities.py +1123 -0
  38. bootstack/assets/__init__.py +21 -0
  39. bootstack/assets/bootstack.ico +0 -0
  40. bootstack/assets/bootstack.png +0 -0
  41. bootstack/assets/elements/__init__.py +0 -0
  42. bootstack/assets/elements/badge-pill.png +0 -0
  43. bootstack/assets/elements/badge-square.png +0 -0
  44. bootstack/assets/elements/button-compact.png +0 -0
  45. bootstack/assets/elements/button-default.png +0 -0
  46. bootstack/assets/elements/buttongroup-after-h-compact.png +0 -0
  47. bootstack/assets/elements/buttongroup-after-h-default.png +0 -0
  48. bootstack/assets/elements/buttongroup-after-v-compact.png +0 -0
  49. bootstack/assets/elements/buttongroup-after-v-default.png +0 -0
  50. bootstack/assets/elements/buttongroup-before-h-compact.png +0 -0
  51. bootstack/assets/elements/buttongroup-before-h-default.png +0 -0
  52. bootstack/assets/elements/buttongroup-before-v-compact.png +0 -0
  53. bootstack/assets/elements/buttongroup-before-v-default.png +0 -0
  54. bootstack/assets/elements/buttongroup-center-h-compact.png +0 -0
  55. bootstack/assets/elements/buttongroup-center-h-default.png +0 -0
  56. bootstack/assets/elements/buttongroup-center-v-compact.png +0 -0
  57. bootstack/assets/elements/buttongroup-center-v-default.png +0 -0
  58. bootstack/assets/elements/card.png +0 -0
  59. bootstack/assets/elements/checkbox-checked.png +0 -0
  60. bootstack/assets/elements/checkbox-indeterminate.png +0 -0
  61. bootstack/assets/elements/checkbox-unchecked.png +0 -0
  62. bootstack/assets/elements/field.png +0 -0
  63. bootstack/assets/elements/input-addon-compact.png +0 -0
  64. bootstack/assets/elements/input-addon-default.png +0 -0
  65. bootstack/assets/elements/input-compact.png +0 -0
  66. bootstack/assets/elements/input-default.png +0 -0
  67. bootstack/assets/elements/list-item-separated.png +0 -0
  68. bootstack/assets/elements/list-item.png +0 -0
  69. bootstack/assets/elements/listrow-compact.png +0 -0
  70. bootstack/assets/elements/listrow-default.png +0 -0
  71. bootstack/assets/elements/manifest.toml +361 -0
  72. bootstack/assets/elements/menu-item.png +0 -0
  73. bootstack/assets/elements/navitem-compact.png +0 -0
  74. bootstack/assets/elements/navitem-default.png +0 -0
  75. bootstack/assets/elements/progressbar-h-compact.png +0 -0
  76. bootstack/assets/elements/progressbar-h-default.png +0 -0
  77. bootstack/assets/elements/progressbar-v-compact.png +0 -0
  78. bootstack/assets/elements/progressbar-v-default.png +0 -0
  79. bootstack/assets/elements/radiobutton.png +0 -0
  80. bootstack/assets/elements/scrollbar-horizontal.png +0 -0
  81. bootstack/assets/elements/scrollbar-vertical.png +0 -0
  82. bootstack/assets/elements/slider-handle.png +0 -0
  83. bootstack/assets/elements/slider-track-h.png +0 -0
  84. bootstack/assets/elements/slider-track-v.png +0 -0
  85. bootstack/assets/elements/switch-off.png +0 -0
  86. bootstack/assets/elements/switch-on.png +0 -0
  87. bootstack/assets/elements/tab-h.png +0 -0
  88. bootstack/assets/elements/tab-v.png +0 -0
  89. bootstack/assets/icons/bootstrap.ttf +0 -0
  90. bootstack/assets/icons/glyphmap.json +2080 -0
  91. bootstack/assets/icons/icon_metrics.json +12470 -0
  92. bootstack/assets/locales/ar/LC_MESSAGES/bootstack.mo +0 -0
  93. bootstack/assets/locales/ar/LC_MESSAGES/bootstack.po +856 -0
  94. bootstack/assets/locales/bg/LC_MESSAGES/bootstack.mo +0 -0
  95. bootstack/assets/locales/bg/LC_MESSAGES/bootstack.po +878 -0
  96. bootstack/assets/locales/cs/LC_MESSAGES/bootstack.mo +0 -0
  97. bootstack/assets/locales/cs/LC_MESSAGES/bootstack.po +856 -0
  98. bootstack/assets/locales/da/LC_MESSAGES/bootstack.mo +0 -0
  99. bootstack/assets/locales/da/LC_MESSAGES/bootstack.po +856 -0
  100. bootstack/assets/locales/de/LC_MESSAGES/bootstack.mo +0 -0
  101. bootstack/assets/locales/de/LC_MESSAGES/bootstack.po +856 -0
  102. bootstack/assets/locales/en/LC_MESSAGES/bootstack.mo +0 -0
  103. bootstack/assets/locales/en/LC_MESSAGES/bootstack.po +878 -0
  104. bootstack/assets/locales/es/LC_MESSAGES/bootstack.mo +0 -0
  105. bootstack/assets/locales/es/LC_MESSAGES/bootstack.po +856 -0
  106. bootstack/assets/locales/fr/LC_MESSAGES/bootstack.mo +0 -0
  107. bootstack/assets/locales/fr/LC_MESSAGES/bootstack.po +856 -0
  108. bootstack/assets/locales/he/LC_MESSAGES/bootstack.mo +0 -0
  109. bootstack/assets/locales/he/LC_MESSAGES/bootstack.po +854 -0
  110. bootstack/assets/locales/hi/LC_MESSAGES/bootstack.mo +0 -0
  111. bootstack/assets/locales/hi/LC_MESSAGES/bootstack.po +845 -0
  112. bootstack/assets/locales/it/LC_MESSAGES/bootstack.mo +0 -0
  113. bootstack/assets/locales/it/LC_MESSAGES/bootstack.po +844 -0
  114. bootstack/assets/locales/ja/LC_MESSAGES/bootstack.mo +0 -0
  115. bootstack/assets/locales/ja/LC_MESSAGES/bootstack.po +917 -0
  116. bootstack/assets/locales/ko/LC_MESSAGES/bootstack.mo +0 -0
  117. bootstack/assets/locales/ko/LC_MESSAGES/bootstack.po +845 -0
  118. bootstack/assets/locales/nb/LC_MESSAGES/bootstack.mo +0 -0
  119. bootstack/assets/locales/nb/LC_MESSAGES/bootstack.po +844 -0
  120. bootstack/assets/locales/nl/LC_MESSAGES/bootstack.mo +0 -0
  121. bootstack/assets/locales/nl/LC_MESSAGES/bootstack.po +844 -0
  122. bootstack/assets/locales/pl/LC_MESSAGES/bootstack.mo +0 -0
  123. bootstack/assets/locales/pl/LC_MESSAGES/bootstack.po +845 -0
  124. bootstack/assets/locales/pt/LC_MESSAGES/bootstack.mo +0 -0
  125. bootstack/assets/locales/pt/LC_MESSAGES/bootstack.po +845 -0
  126. bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.mo +0 -0
  127. bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.po +845 -0
  128. bootstack/assets/locales/sl/LC_MESSAGES/bootstack.mo +0 -0
  129. bootstack/assets/locales/sl/LC_MESSAGES/bootstack.po +845 -0
  130. bootstack/assets/locales/sv/LC_MESSAGES/bootstack.mo +0 -0
  131. bootstack/assets/locales/sv/LC_MESSAGES/bootstack.po +845 -0
  132. bootstack/assets/locales/tr/LC_MESSAGES/bootstack.mo +0 -0
  133. bootstack/assets/locales/tr/LC_MESSAGES/bootstack.po +845 -0
  134. bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.mo +0 -0
  135. bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.po +845 -0
  136. bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.mo +0 -0
  137. bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.po +845 -0
  138. bootstack/cli/__init__.py +133 -0
  139. bootstack/cli/__main__.py +6 -0
  140. bootstack/cli/add.py +395 -0
  141. bootstack/cli/appicon.py +285 -0
  142. bootstack/cli/build.py +115 -0
  143. bootstack/cli/config.py +313 -0
  144. bootstack/cli/demo.py +564 -0
  145. bootstack/cli/dev.py +153 -0
  146. bootstack/cli/doctor.py +195 -0
  147. bootstack/cli/icons.py +98 -0
  148. bootstack/cli/promote.py +120 -0
  149. bootstack/cli/pyinstaller.py +268 -0
  150. bootstack/cli/run.py +95 -0
  151. bootstack/cli/start.py +117 -0
  152. bootstack/cli/templates/__init__.py +931 -0
  153. bootstack/clipboard.py +48 -0
  154. bootstack/constants.py +318 -0
  155. bootstack/data/README.md +615 -0
  156. bootstack/data/__init__.py +78 -0
  157. bootstack/data/_observable.py +276 -0
  158. bootstack/data/base.py +780 -0
  159. bootstack/data/file_source.py +367 -0
  160. bootstack/data/memory_source.py +450 -0
  161. bootstack/data/query.py +367 -0
  162. bootstack/data/readers.py +289 -0
  163. bootstack/data/sqlite_source.py +869 -0
  164. bootstack/data/types.py +354 -0
  165. bootstack/data/writers.py +232 -0
  166. bootstack/dev/__init__.py +36 -0
  167. bootstack/dev/_capture.py +141 -0
  168. bootstack/dev/_env.py +43 -0
  169. bootstack/dev/_registry.py +140 -0
  170. bootstack/dev/_reloader.py +351 -0
  171. bootstack/dev/_reset.py +50 -0
  172. bootstack/dev/_watcher.py +91 -0
  173. bootstack/dialogs/__init__.py +948 -0
  174. bootstack/dialogs/_impl/__init__.py +47 -0
  175. bootstack/dialogs/_impl/colorchooser.py +588 -0
  176. bootstack/dialogs/_impl/datedialog.py +450 -0
  177. bootstack/dialogs/_impl/dialog.py +594 -0
  178. bootstack/dialogs/_impl/filterdialog.py +358 -0
  179. bootstack/dialogs/_impl/fontdialog.py +364 -0
  180. bootstack/dialogs/_impl/formdialog.py +564 -0
  181. bootstack/dialogs/_impl/message.py +486 -0
  182. bootstack/dialogs/_impl/query.py +570 -0
  183. bootstack/errors.py +67 -0
  184. bootstack/events/__init__.py +111 -0
  185. bootstack/events/_event.py +135 -0
  186. bootstack/events/_payloads.py +539 -0
  187. bootstack/events/_subscription.py +38 -0
  188. bootstack/i18n/README.md +77 -0
  189. bootstack/i18n/__init__.py +23 -0
  190. bootstack/i18n/catalog.py +121 -0
  191. bootstack/i18n/intl_format.py +584 -0
  192. bootstack/i18n/msgcat.py +425 -0
  193. bootstack/i18n/specs.py +156 -0
  194. bootstack/images.py +563 -0
  195. bootstack/py.typed +1 -0
  196. bootstack/scheduling/__init__.py +11 -0
  197. bootstack/scheduling/_schedule.py +218 -0
  198. bootstack/shortcuts.py +21 -0
  199. bootstack/signals/README.md +98 -0
  200. bootstack/signals/__init__.py +10 -0
  201. bootstack/signals/integration.py +100 -0
  202. bootstack/signals/signal.py +353 -0
  203. bootstack/signals/types.py +6 -0
  204. bootstack/store.py +286 -0
  205. bootstack/streams/__init__.py +12 -0
  206. bootstack/streams/_stream.py +321 -0
  207. bootstack/style/__init__.py +38 -0
  208. bootstack/style/builders/__init__.py +51 -0
  209. bootstack/style/builders/badge.py +46 -0
  210. bootstack/style/builders/button.py +339 -0
  211. bootstack/style/builders/buttongroup.py +311 -0
  212. bootstack/style/builders/calendar.py +271 -0
  213. bootstack/style/builders/checkbutton.py +110 -0
  214. bootstack/style/builders/combobox.py +113 -0
  215. bootstack/style/builders/contextmenu.py +268 -0
  216. bootstack/style/builders/entry.py +82 -0
  217. bootstack/style/builders/expander.py +148 -0
  218. bootstack/style/builders/field.py +335 -0
  219. bootstack/style/builders/frame.py +50 -0
  220. bootstack/style/builders/label.py +28 -0
  221. bootstack/style/builders/labelframe.py +34 -0
  222. bootstack/style/builders/listview.py +369 -0
  223. bootstack/style/builders/menubar.py +91 -0
  224. bootstack/style/builders/menubutton.py +359 -0
  225. bootstack/style/builders/panedwindow.py +25 -0
  226. bootstack/style/builders/progressbar.py +67 -0
  227. bootstack/style/builders/radiobutton.py +99 -0
  228. bootstack/style/builders/scale.py +64 -0
  229. bootstack/style/builders/scrollbar.py +225 -0
  230. bootstack/style/builders/separator.py +49 -0
  231. bootstack/style/builders/sidenav.py +643 -0
  232. bootstack/style/builders/sizegrip.py +15 -0
  233. bootstack/style/builders/spinbox.py +119 -0
  234. bootstack/style/builders/switch.py +70 -0
  235. bootstack/style/builders/tabitem.py +204 -0
  236. bootstack/style/builders/togglegroup.py +294 -0
  237. bootstack/style/builders/toolbutton.py +275 -0
  238. bootstack/style/builders/tooltip.py +26 -0
  239. bootstack/style/builders/treeview.py +193 -0
  240. bootstack/style/builders/utils.py +455 -0
  241. bootstack/style/builders_tk/__init__.py +16 -0
  242. bootstack/style/builders_tk/defaults.py +229 -0
  243. bootstack/style/element.py +173 -0
  244. bootstack/style/fonts.py +123 -0
  245. bootstack/style/style.py +609 -0
  246. bootstack/style/style_builder_base.py +716 -0
  247. bootstack/style/style_builder_mixed.py +93 -0
  248. bootstack/style/style_builder_tk.py +109 -0
  249. bootstack/style/style_builder_ttk.py +353 -0
  250. bootstack/style/style_resolver.py +447 -0
  251. bootstack/style/theme.py +245 -0
  252. bootstack/style/theme_provider.py +471 -0
  253. bootstack/style/themes/__init__.py +128 -0
  254. bootstack/style/tk_patch.py +5 -0
  255. bootstack/style/token_maps.py +41 -0
  256. bootstack/style/types.py +32 -0
  257. bootstack/style/typography.py +523 -0
  258. bootstack/style/utility.py +746 -0
  259. bootstack/types.py +39 -0
  260. bootstack/validation/__init__.py +6 -0
  261. bootstack/validation/types.py +15 -0
  262. bootstack/validation/validation_result.py +17 -0
  263. bootstack/validation/validation_rules.py +205 -0
  264. bootstack/widgets/__init__.py +74 -0
  265. bootstack/widgets/_core/__init__.py +31 -0
  266. bootstack/widgets/_core/app_config.py +266 -0
  267. bootstack/widgets/_core/base.py +461 -0
  268. bootstack/widgets/_core/container.py +334 -0
  269. bootstack/widgets/_core/context.py +35 -0
  270. bootstack/widgets/_core/events.py +130 -0
  271. bootstack/widgets/_core/field_mixin.py +353 -0
  272. bootstack/widgets/_core/icon_image_props.py +72 -0
  273. bootstack/widgets/_core/image_binding.py +93 -0
  274. bootstack/widgets/_core/navmodel.py +345 -0
  275. bootstack/widgets/_core/options.py +210 -0
  276. bootstack/widgets/_core/selection_group.py +130 -0
  277. bootstack/widgets/_core/window_controls.py +96 -0
  278. bootstack/widgets/_core/window_menu.py +246 -0
  279. bootstack/widgets/_impl/__init__.py +1 -0
  280. bootstack/widgets/_impl/_internal/__init__.py +0 -0
  281. bootstack/widgets/_impl/_internal/wrapper_base.py +307 -0
  282. bootstack/widgets/_impl/_parts/__init__.py +11 -0
  283. bootstack/widgets/_impl/_parts/numberentry_part.py +385 -0
  284. bootstack/widgets/_impl/_parts/spinnerentry_part.py +434 -0
  285. bootstack/widgets/_impl/_parts/textentry_part.py +406 -0
  286. bootstack/widgets/_impl/composites/__init__.py +33 -0
  287. bootstack/widgets/_impl/composites/_dateutils.py +33 -0
  288. bootstack/widgets/_impl/composites/_image_fit.py +105 -0
  289. bootstack/widgets/_impl/composites/accordion.py +381 -0
  290. bootstack/widgets/_impl/composites/avatar.py +191 -0
  291. bootstack/widgets/_impl/composites/buttongroup.py +371 -0
  292. bootstack/widgets/_impl/composites/calendar.py +972 -0
  293. bootstack/widgets/_impl/composites/carousel.py +567 -0
  294. bootstack/widgets/_impl/composites/chart.py +882 -0
  295. bootstack/widgets/_impl/composites/compositeframe.py +298 -0
  296. bootstack/widgets/_impl/composites/contextmenu.py +1951 -0
  297. bootstack/widgets/_impl/composites/dateentry.py +404 -0
  298. bootstack/widgets/_impl/composites/dropdownbutton.py +325 -0
  299. bootstack/widgets/_impl/composites/expander.py +515 -0
  300. bootstack/widgets/_impl/composites/field.py +670 -0
  301. bootstack/widgets/_impl/composites/form.py +1066 -0
  302. bootstack/widgets/_impl/composites/gallery.py +551 -0
  303. bootstack/widgets/_impl/composites/list/__init__.py +15 -0
  304. bootstack/widgets/_impl/composites/list/listitem.py +802 -0
  305. bootstack/widgets/_impl/composites/list/listview.py +1433 -0
  306. bootstack/widgets/_impl/composites/menu/__init__.py +18 -0
  307. bootstack/widgets/_impl/composites/menu/model.py +358 -0
  308. bootstack/widgets/_impl/composites/menu/render_native.py +134 -0
  309. bootstack/widgets/_impl/composites/menu/render_themed.py +134 -0
  310. bootstack/widgets/_impl/composites/meter.py +860 -0
  311. bootstack/widgets/_impl/composites/numericentry.py +201 -0
  312. bootstack/widgets/_impl/composites/pagestack.py +395 -0
  313. bootstack/widgets/_impl/composites/passwordentry.py +142 -0
  314. bootstack/widgets/_impl/composites/pathentry.py +168 -0
  315. bootstack/widgets/_impl/composites/picture.py +289 -0
  316. bootstack/widgets/_impl/composites/radiogroup.py +511 -0
  317. bootstack/widgets/_impl/composites/scrolledtext.py +375 -0
  318. bootstack/widgets/_impl/composites/scrolledtext.pyi +186 -0
  319. bootstack/widgets/_impl/composites/scrollview.py +764 -0
  320. bootstack/widgets/_impl/composites/selectbox.py +1026 -0
  321. bootstack/widgets/_impl/composites/shell/__init__.py +40 -0
  322. bootstack/widgets/_impl/composites/shell/content_host.py +52 -0
  323. bootstack/widgets/_impl/composites/shell/layout.py +335 -0
  324. bootstack/widgets/_impl/composites/shell/nav_panel.py +345 -0
  325. bootstack/widgets/_impl/composites/shell/providers.py +558 -0
  326. bootstack/widgets/_impl/composites/shell/rail.py +117 -0
  327. bootstack/widgets/_impl/composites/shell/shell.py +581 -0
  328. bootstack/widgets/_impl/composites/shell/workspace.py +273 -0
  329. bootstack/widgets/_impl/composites/sidenav/__init__.py +16 -0
  330. bootstack/widgets/_impl/composites/sidenav/header.py +81 -0
  331. bootstack/widgets/_impl/composites/sidenav/separator.py +44 -0
  332. bootstack/widgets/_impl/composites/slider/__init__.py +7 -0
  333. bootstack/widgets/_impl/composites/slider/_shared.py +195 -0
  334. bootstack/widgets/_impl/composites/slider/rangeslider.py +982 -0
  335. bootstack/widgets/_impl/composites/slider/slider.py +851 -0
  336. bootstack/widgets/_impl/composites/spinnerentry.py +185 -0
  337. bootstack/widgets/_impl/composites/tableview/__init__.py +5 -0
  338. bootstack/widgets/_impl/composites/tableview/tableview.py +3388 -0
  339. bootstack/widgets/_impl/composites/tableview/types.py +169 -0
  340. bootstack/widgets/_impl/composites/tabs/__init__.py +23 -0
  341. bootstack/widgets/_impl/composites/tabs/tabitem.py +389 -0
  342. bootstack/widgets/_impl/composites/tabs/tabs.py +974 -0
  343. bootstack/widgets/_impl/composites/tabs/tabview.py +650 -0
  344. bootstack/widgets/_impl/composites/textarea/__init__.py +26 -0
  345. bootstack/widgets/_impl/composites/textarea/change.py +46 -0
  346. bootstack/widgets/_impl/composites/textarea/codeeditor.py +526 -0
  347. bootstack/widgets/_impl/composites/textarea/core.py +495 -0
  348. bootstack/widgets/_impl/composites/textarea/decoration.py +42 -0
  349. bootstack/widgets/_impl/composites/textarea/diff.py +127 -0
  350. bootstack/widgets/_impl/composites/textarea/extensions/__init__.py +1 -0
  351. bootstack/widgets/_impl/composites/textarea/extensions/bracket_matcher.py +138 -0
  352. bootstack/widgets/_impl/composites/textarea/extensions/indent_guides.py +158 -0
  353. bootstack/widgets/_impl/composites/textarea/extensions/line_numbers.py +138 -0
  354. bootstack/widgets/_impl/composites/textarea/extensions/pygments_highlighter.py +312 -0
  355. bootstack/widgets/_impl/composites/textarea/extensions/smart_indent.py +177 -0
  356. bootstack/widgets/_impl/composites/textarea/filter.py +171 -0
  357. bootstack/widgets/_impl/composites/textarea/search_overlay.py +459 -0
  358. bootstack/widgets/_impl/composites/textarea/sidebar.py +88 -0
  359. bootstack/widgets/_impl/composites/textarea/style_registry.py +178 -0
  360. bootstack/widgets/_impl/composites/textarea/textarea.py +606 -0
  361. bootstack/widgets/_impl/composites/textarea/undo.py +217 -0
  362. bootstack/widgets/_impl/composites/textentry.py +57 -0
  363. bootstack/widgets/_impl/composites/timeentry.py +176 -0
  364. bootstack/widgets/_impl/composites/toast.py +390 -0
  365. bootstack/widgets/_impl/composites/toast_stack.py +156 -0
  366. bootstack/widgets/_impl/composites/togglegroup.py +418 -0
  367. bootstack/widgets/_impl/composites/toolbar.py +608 -0
  368. bootstack/widgets/_impl/composites/tooltip.py +491 -0
  369. bootstack/widgets/_impl/composites/tree/__init__.py +7 -0
  370. bootstack/widgets/_impl/composites/tree/source_binding.py +136 -0
  371. bootstack/widgets/_impl/composites/tree/treeitem.py +393 -0
  372. bootstack/widgets/_impl/composites/tree/treenode.py +174 -0
  373. bootstack/widgets/_impl/composites/tree/treeview.py +841 -0
  374. bootstack/widgets/_impl/mixins/__init__.py +24 -0
  375. bootstack/widgets/_impl/mixins/configure_mixin.py +216 -0
  376. bootstack/widgets/_impl/mixins/entry_mixin.py +134 -0
  377. bootstack/widgets/_impl/mixins/font_mixin.py +368 -0
  378. bootstack/widgets/_impl/mixins/icon_mixin.py +61 -0
  379. bootstack/widgets/_impl/mixins/localization_mixin.py +253 -0
  380. bootstack/widgets/_impl/mixins/signal_mixin.py +268 -0
  381. bootstack/widgets/_impl/mixins/validation_mixin.py +226 -0
  382. bootstack/widgets/_impl/primitives/__init__.py +49 -0
  383. bootstack/widgets/_impl/primitives/_menubutton.py +107 -0
  384. bootstack/widgets/_impl/primitives/badge.py +45 -0
  385. bootstack/widgets/_impl/primitives/button.py +76 -0
  386. bootstack/widgets/_impl/primitives/card.py +45 -0
  387. bootstack/widgets/_impl/primitives/checkbutton.py +124 -0
  388. bootstack/widgets/_impl/primitives/checktoggle.py +62 -0
  389. bootstack/widgets/_impl/primitives/combobox.py +156 -0
  390. bootstack/widgets/_impl/primitives/entry.py +87 -0
  391. bootstack/widgets/_impl/primitives/flexframe.py +448 -0
  392. bootstack/widgets/_impl/primitives/frame.py +185 -0
  393. bootstack/widgets/_impl/primitives/gridframe.py +546 -0
  394. bootstack/widgets/_impl/primitives/label.py +84 -0
  395. bootstack/widgets/_impl/primitives/labelframe.py +54 -0
  396. bootstack/widgets/_impl/primitives/optionmenu.py +387 -0
  397. bootstack/widgets/_impl/primitives/packframe.py +227 -0
  398. bootstack/widgets/_impl/primitives/panedwindow.py +45 -0
  399. bootstack/widgets/_impl/primitives/progressbar.py +83 -0
  400. bootstack/widgets/_impl/primitives/radiobutton.py +115 -0
  401. bootstack/widgets/_impl/primitives/radiotoggle.py +54 -0
  402. bootstack/widgets/_impl/primitives/scrollbar.py +42 -0
  403. bootstack/widgets/_impl/primitives/separator.py +43 -0
  404. bootstack/widgets/_impl/primitives/sizegrip.py +33 -0
  405. bootstack/widgets/_impl/primitives/spinbox.py +95 -0
  406. bootstack/widgets/_impl/primitives/switch.py +44 -0
  407. bootstack/widgets/_impl/primitives/treeview.py +69 -0
  408. bootstack/widgets/app.py +371 -0
  409. bootstack/widgets/appshell.py +1179 -0
  410. bootstack/widgets/avatar.py +140 -0
  411. bootstack/widgets/boolean_controls.py +455 -0
  412. bootstack/widgets/button.py +224 -0
  413. bootstack/widgets/buttongroup.py +239 -0
  414. bootstack/widgets/calendar.py +195 -0
  415. bootstack/widgets/card.py +159 -0
  416. bootstack/widgets/carousel.py +241 -0
  417. bootstack/widgets/chart.py +302 -0
  418. bootstack/widgets/codeeditor.py +675 -0
  419. bootstack/widgets/contextmenu.py +371 -0
  420. bootstack/widgets/datatable.py +688 -0
  421. bootstack/widgets/datefield.py +395 -0
  422. bootstack/widgets/divider.py +60 -0
  423. bootstack/widgets/expander.py +579 -0
  424. bootstack/widgets/form.py +200 -0
  425. bootstack/widgets/gallery.py +250 -0
  426. bootstack/widgets/gauge.py +168 -0
  427. bootstack/widgets/grid.py +121 -0
  428. bootstack/widgets/groupbox.py +162 -0
  429. bootstack/widgets/label.py +229 -0
  430. bootstack/widgets/listview.py +354 -0
  431. bootstack/widgets/menubutton.py +419 -0
  432. bootstack/widgets/numberfield.py +431 -0
  433. bootstack/widgets/pagestack.py +345 -0
  434. bootstack/widgets/passwordfield.py +383 -0
  435. bootstack/widgets/pathfield.py +454 -0
  436. bootstack/widgets/picture.py +251 -0
  437. bootstack/widgets/progressbar.py +106 -0
  438. bootstack/widgets/radio_variants.py +271 -0
  439. bootstack/widgets/radiogroup.py +228 -0
  440. bootstack/widgets/scrollbar.py +64 -0
  441. bootstack/widgets/scrollview.py +186 -0
  442. bootstack/widgets/select.py +302 -0
  443. bootstack/widgets/selectbutton.py +182 -0
  444. bootstack/widgets/sidebar_toggle.py +130 -0
  445. bootstack/widgets/sizegrip.py +42 -0
  446. bootstack/widgets/slider.py +408 -0
  447. bootstack/widgets/spinbox.py +147 -0
  448. bootstack/widgets/spinnerfield.py +413 -0
  449. bootstack/widgets/splash.py +367 -0
  450. bootstack/widgets/splitview.py +482 -0
  451. bootstack/widgets/stacks.py +249 -0
  452. bootstack/widgets/statusbar.py +189 -0
  453. bootstack/widgets/tabs.py +425 -0
  454. bootstack/widgets/textarea.py +459 -0
  455. bootstack/widgets/textfield.py +410 -0
  456. bootstack/widgets/theme_toggle.py +92 -0
  457. bootstack/widgets/timefield.py +340 -0
  458. bootstack/widgets/toast.py +314 -0
  459. bootstack/widgets/togglegroup.py +231 -0
  460. bootstack/widgets/toolbar.py +347 -0
  461. bootstack/widgets/tooltip.py +94 -0
  462. bootstack/widgets/tree.py +648 -0
  463. bootstack/widgets/types.py +305 -0
  464. bootstack/widgets/window.py +307 -0
  465. bootstack-0.1.0.dist-info/METADATA +301 -0
  466. bootstack-0.1.0.dist-info/RECORD +471 -0
  467. bootstack-0.1.0.dist-info/WHEEL +5 -0
  468. bootstack-0.1.0.dist-info/entry_points.txt +2 -0
  469. bootstack-0.1.0.dist-info/licenses/LICENSE +21 -0
  470. bootstack-0.1.0.dist-info/licenses/NOTICE +10 -0
  471. bootstack-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,716 @@
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 bootstack.style.theme_provider import ThemeProvider, use_theme
10
+ from bootstack.style.utility import best_foreground, color_to_hsl, darken_color, lighten_color, mix_colors, \
11
+ muted_foreground as _muted_foreground, relative_luminance
12
+ from bootstack._runtime.utility import scale_size
13
+
14
+ # Source images are created at this resolution multiplier (e.g., 2x)
15
+ # This allows measurements from source images to be used directly in code
16
+ SOURCE_RESOLUTION = 2.0
17
+
18
+
19
+ class IconSpec(TypedDict, total=False):
20
+ name: str
21
+ size: Optional[int]
22
+ color: Optional[str]
23
+ state: Sequence[tuple[str, str | IconStateMap]]
24
+
25
+
26
+ class IconStateMap(TypedDict, total=False):
27
+ name: Optional[str]
28
+ color: Optional[str]
29
+
30
+
31
+ ForegroundStateSpec = tuple[str, str | dict[str, str]]
32
+
33
+
34
+ class StyleBuilderBase:
35
+ """Shared base for TTK and Tk style builders.
36
+
37
+ Centralizes theme provider plumbing and color utilities so both
38
+ StyleBuilder (TTK) and StyleBuilderTk (Tk) can inherit and
39
+ avoid duplication.
40
+ """
41
+
42
+ _RE_TOKEN = re.compile(r"^\s*(?P<head>[a-zA-Z_][\w-]*)(?P<brackets>(?:\[[^]]*])*)\s*$")
43
+
44
+ def __init__(self, theme_provider: ThemeProvider | None = None, style_instance: Any | None = None): # noqa: ANN401
45
+ # If no provider given, try to derive from style_instance
46
+ if theme_provider is None and style_instance is not None:
47
+ try:
48
+ theme_provider = style_instance.theme_provider # type: ignore[attr-defined]
49
+ except Exception:
50
+ theme_provider = None
51
+ self._provider = theme_provider or use_theme()
52
+ self._style = style_instance
53
+
54
+ def set_style_instance(self, style_instance: Any) -> None: # noqa: ANN401
55
+ self._style = style_instance
56
+
57
+ @property
58
+ def provider(self) -> ThemeProvider:
59
+ return self._provider
60
+
61
+ @property
62
+ def style(self) -> Any | None: # noqa: ANN401
63
+ return self._style
64
+
65
+ @property
66
+ def colors(self) -> dict:
67
+ return self.provider.colors
68
+
69
+ # ----- Color Utilities & Transformers -----
70
+
71
+ @staticmethod
72
+ def default(value: Optional[str] = None):
73
+ if value == 'default':
74
+ return None
75
+ return value
76
+
77
+ def _parse_color_token(self, token: str):
78
+ """Parse a color token into head and ordered list of modifiers.
79
+
80
+ Returns a dict with:
81
+ - head: The base color name (e.g., "primary")
82
+ - modifiers: List of (type, value) tuples in order
83
+ """
84
+ m = self._RE_TOKEN.match(token)
85
+ if not m:
86
+ return None
87
+
88
+ head = m.group("head")
89
+ brackets_raw = m.group("brackets")
90
+
91
+ modifiers = [] # List of (type, value) tuples in order
92
+
93
+ if brackets_raw:
94
+ # Extract all bracket contents: [content1][content2] -> ["content1", "content2"]
95
+ bracket_contents = re.findall(r'\[([^]]*)]', brackets_raw)
96
+
97
+ # Process each bracket in order for pipeline
98
+ for bracket in bracket_contents:
99
+ part = bracket.strip().lower()
100
+ if not part:
101
+ continue
102
+
103
+ if part.isdigit():
104
+ modifiers.append(("shade", int(part)))
105
+ elif re.fullmatch(r'[+-]\s*\d+(?:\.\d+)?', part):
106
+ # Fractional steps allowed (e.g. "+0.85") for fine surface
107
+ # tints; elevate() blends continuously, so floats are exact.
108
+ rel = float(part.replace(" ", ""))
109
+ modifiers.append(("elevation", rel))
110
+ elif part == "subtle":
111
+ modifiers.append(("subtle", None))
112
+ elif part == "muted":
113
+ modifiers.append(("muted", None))
114
+
115
+ return {"head": head, "modifiers": modifiers}
116
+
117
+ def color(self, token: str, surface: str | None = None, role: str = "background") -> str:
118
+ """Resolve a color token with optional chained modifiers.
119
+
120
+ Modifiers are applied as a pipeline from left to right:
121
+ - primary[100][muted] → lookup primary[100], then apply muted
122
+ - background[+1][muted] → lookup background, elevate it, then apply muted
123
+
124
+ Args:
125
+ token: Color token (e.g., "primary", "primary[100][muted]")
126
+ Can also be a hex color string (e.g., "#ff0000")
127
+ surface: Optional surface color for subtle modifier
128
+ role: Role for subtle modifier ("background" or "text")
129
+
130
+ Returns:
131
+ The resolved color value as a hex string
132
+
133
+ Raises:
134
+ ValueError: If the color token cannot be resolved to a valid color.
135
+ """
136
+ # If token is already a hex color, return it directly
137
+ if token and token.startswith('#'):
138
+ return token
139
+
140
+ # Fast path: exact key (e.g., "blue[100]" or "primary")
141
+ direct = self.colors.get(token)
142
+ if direct is not None:
143
+ return direct
144
+
145
+ parsed = self._parse_color_token(token)
146
+ if not parsed:
147
+ result = self.colors.get(token)
148
+ if result is None:
149
+ raise ValueError(
150
+ f"Invalid color token: '{token}'. "
151
+ f"Valid color tokens include: primary, success, warning, danger, "
152
+ f"background, foreground, or a hex color like '#ff0000'."
153
+ )
154
+ return result
155
+
156
+ head = parsed["head"]
157
+ modifiers = parsed["modifiers"]
158
+
159
+ # Determine base lookup key
160
+ # If first modifier is a shade, use it for lookup and remove from pipeline
161
+ base_key = head
162
+ if modifiers and modifiers[0][0] == "shade":
163
+ shade_value = modifiers[0][1]
164
+ base_key = f"{head}[{shade_value}]"
165
+ modifiers = modifiers[1:] # Remove shade from pipeline
166
+
167
+ # Get the base color
168
+ current_color = self.colors.get(base_key)
169
+ if current_color is None:
170
+ # Unknown base; fall back
171
+ result = self.colors.get(token)
172
+ if result is None:
173
+ raise ValueError(
174
+ f"Invalid color token: '{token}'. "
175
+ f"Valid color tokens include: primary, success, warning, danger, "
176
+ f"background, foreground, or a hex color like '#ff0000'."
177
+ )
178
+ return result
179
+
180
+ # Apply each modifier in order as a pipeline transformation
181
+ for mod_type, mod_value in modifiers:
182
+ if mod_type == "elevation":
183
+ current_color = self.elevate(current_color, mod_value)
184
+ elif mod_type == "subtle":
185
+ # Subtle needs special handling - it does its own lookup
186
+ # Use the base_key for lookup, not the current transformed color
187
+ current_color = self.subtle(base_key, surface, role)
188
+ elif mod_type == "muted":
189
+ # Muted transforms whatever color we currently have
190
+ current_color = self.muted_foreground(current_color)
191
+
192
+ return current_color
193
+
194
+ def subtle(self, token: str, surface: str | None = None, role: str = "background") -> str:
195
+ """Return a subtle instance of this color for background or text."""
196
+ if token and token.startswith('#'):
197
+ base_color = token
198
+ else:
199
+ parsed = self._parse_color_token(token)
200
+ if parsed:
201
+ base_key = parsed["head"]
202
+ modifiers = parsed["modifiers"]
203
+ if modifiers and modifiers[0][0] == "shade":
204
+ base_key = f"{base_key}[{modifiers[0][1]}]"
205
+ base_color = self.colors.get(base_key)
206
+ else:
207
+ base_color = self.colors.get(token)
208
+
209
+ if base_color is None:
210
+ base_color = self.colors.get(token) or self.colors.get('foreground')
211
+
212
+ surface_val = surface or self.colors.get('background')
213
+
214
+ if role == "text":
215
+ # Prefer the precomputed per-semantic emphasis token (a readable,
216
+ # accent-tinted text color), else darken/lighten the base.
217
+ emphasis = self.colors.get(f"{token}_emphasis") if isinstance(token, str) else None
218
+ if emphasis:
219
+ return emphasis
220
+ if self.provider.mode == "light":
221
+ return darken_color(base_color, 0.25)
222
+ else:
223
+ return lighten_color(base_color, 0.25)
224
+ else: # background
225
+ # Blend the accent into the surface — surface-relative, so the wash
226
+ # reads in BOTH modes (a pure shade like [900] is surface-blind and
227
+ # lands darker than a dark surface, making the selection recede). Use
228
+ # the mode's accent stop: [500] (the bright anchor) on light, [400]
229
+ # (the dark-optimized, brighter accent) on dark so low-luminance hues
230
+ # like blue still lift off the dark surface. A gentler blend on light
231
+ # (the surface is bright, so a little tint reads); a touch more on
232
+ # dark where the wash must lift to register.
233
+ if self.provider.mode == "light":
234
+ stop, fraction = "500", 0.14
235
+ else:
236
+ stop, fraction = "400", 0.20
237
+ anchor = self.colors.get(f"{token}[{stop}]") if isinstance(token, str) else None
238
+ blend_into = anchor if anchor else base_color
239
+ return mix_colors(blend_into, surface_val, fraction)
240
+
241
+ def active(self, color: str) -> str:
242
+ return self._state_color(color, "active")
243
+
244
+ def pressed(self, color: str) -> str:
245
+ return self._state_color(color, "pressed")
246
+
247
+ def focus(self, color: str) -> str:
248
+ return self._state_color(color, "focus")
249
+
250
+ def selected(self, color: str) -> str:
251
+ return self._state_color(color, "selected")
252
+
253
+ def focus_border(self, color: str) -> str:
254
+ lum = relative_luminance(color)
255
+ if self.provider.mode == "dark":
256
+ return lighten_color(color, 0.1)
257
+ else:
258
+ return darken_color(color, 0.2 if lum > 0.5 else 0.1)
259
+
260
+ def focus_ring(self, color: str, surface: str | None = None) -> str:
261
+ surface = surface or self.color(color)
262
+ lum = relative_luminance(color)
263
+ if self.provider.mode == "dark":
264
+ if lum < 0.3:
265
+ brightened = lighten_color(color, 0.2)
266
+ mixed = mix_colors(brightened, surface, 0.2)
267
+ else:
268
+ mixed = mix_colors(color, surface, 0.3)
269
+ else:
270
+ if lum > 0.5:
271
+ blended = mix_colors(color, surface, 0.2)
272
+ mixed = darken_color(blended, 0.15)
273
+ else:
274
+ brightened = lighten_color(color, 0.25)
275
+ mixed = mix_colors(brightened, surface, 0.25)
276
+ return mixed
277
+
278
+ def focus_inner(self, fill: str) -> str:
279
+ """Internal focus line (2–3px), slightly brighter but not glowy."""
280
+ from bootstack.style.utility import contrast_ratio, hex_to_rgb
281
+
282
+ on = self.on_color(fill)
283
+
284
+ # Brighter defaults
285
+ w = 0.26 if self.provider.mode == "light" else 0.20
286
+ ring = mix_colors(on, fill, w)
287
+
288
+ # If still not distinct enough, bump once (bounded)
289
+ try:
290
+ bg = hex_to_rgb(fill)
291
+ fg = hex_to_rgb(ring)
292
+ if contrast_ratio(bg, fg) < 2.4:
293
+ w2 = min(w + 0.08, 0.34 if self.provider.mode == "light" else 0.28)
294
+ ring = mix_colors(on, fill, w2)
295
+ except Exception:
296
+ pass
297
+
298
+ return ring
299
+
300
+ def border(self, color: str, strength: float = 0.84) -> str:
301
+ """Derive a stroke color for a given surface by blending toward the surface's
302
+ computed on-color (text/icon color).
303
+ """
304
+ fg = self.on_color(color)
305
+ return mix_colors(color, fg, strength)
306
+
307
+ def on_color(self, color: str) -> str:
308
+ """Return a readable foreground color for the given background.
309
+
310
+ This is intentionally biased so that *dark-ish* accents get light
311
+ text rather than the mathematically highest-contrast dark text,
312
+ which tends to look wrong on buttons and pills.
313
+ """
314
+ background = self.color("background")
315
+ foreground = self.color("foreground")
316
+
317
+ try:
318
+ lum = relative_luminance(color)
319
+ except Exception:
320
+ candidates = [foreground, background, "#000000", "#ffffff"]
321
+ return best_foreground(color, candidates)
322
+
323
+ # Optional HSL-based accent detection to handle saturated mid-tone
324
+ # colors (like teal) that visually need light text even when their
325
+ # raw luminance is not very low.
326
+ hue = sat = hsl_lum = None
327
+ try:
328
+ hue, sat, hsl_lum = color_to_hsl(color, model="hex")
329
+ except Exception:
330
+ pass
331
+
332
+ if self.provider.mode == "light":
333
+ accent_force_light = False
334
+ if hue is not None and sat is not None and hsl_lum is not None:
335
+ # Treat saturated accents as needing light text when they are
336
+ # not extremely light overall. This catches teal/cyan/etc. but
337
+ # avoids triggering on near-white tinted backgrounds.
338
+ if sat >= 40 and hsl_lum <= 80:
339
+ # Strong teal/cyan/blue band (e.g., teal[600], cyan[600])
340
+ if 140 <= hue <= 220:
341
+ accent_force_light = True
342
+ # Other saturated accents, excluding the yellow/orange band,
343
+ # when not extremely light.
344
+ elif hsl_lum <= 70 and not (35 <= hue <= 70):
345
+ accent_force_light = True
346
+
347
+ # Never force white when white is actually unreadable. A bright but
348
+ # mid-luminance accent like cyan (info) or yellow (warning) has far
349
+ # higher contrast with black than white, so it must take dark text —
350
+ # the luminance buckets alone would wrongly pick white. Gate the
351
+ # light-text bias on white clearing a real WCAG threshold.
352
+ #
353
+ # Use the large-text / UI threshold (3.0), not the normal-text 4.5:
354
+ # accent text is button/pill/badge labels (bold, >=14pt), so 3.0 is the
355
+ # correct WCAG bar. 4.5 was too strict and sent saturated mid-tone
356
+ # accents (e.g. dracula-light's purple primary, white ~3.7) to black,
357
+ # making them inconsistent with the same accent in dark mode (white).
358
+ contrast_white = 1.05 / (lum + 0.05) # ratio of #ffffff vs the color
359
+ white_ok = contrast_white >= 3.0
360
+
361
+ # Saturated / dark-ish accents prefer white text — but only when it
362
+ # is legible; otherwise fall through to a contrast-chosen dark color.
363
+ if (accent_force_light or lum <= 0.55) and white_ok:
364
+ candidates = ["#ffffff"]
365
+ # Mid-light colors (e.g., warning, info): let contrast choose, with a
366
+ # bias toward the theme foreground.
367
+ elif lum <= 0.80:
368
+ candidates = [foreground, "#000000", "#ffffff"]
369
+ # Very light backgrounds -> dark text
370
+ else:
371
+ candidates = [foreground, "#000000"]
372
+ else:
373
+ # Dark mode: inverse bias.
374
+ if lum >= 0.75:
375
+ # Very light chips / badges -> dark text
376
+ candidates = ["#000000", foreground]
377
+ elif lum >= 0.45:
378
+ # Mid tones -> let contrast pick, but include light text
379
+ candidates = ["#ffffff", foreground, "#000000"]
380
+ else:
381
+ # Very dark surfaces -> light text
382
+ candidates = ["#ffffff", foreground]
383
+
384
+ # Deduplicate and remove empty candidates
385
+ unique: list[str] = []
386
+ for c in candidates:
387
+ if c and c not in unique:
388
+ unique.append(c)
389
+
390
+ chosen = best_foreground(color, unique)
391
+
392
+ # Safety floor (3.0 = the large/bold WCAG bar that accent labels meet).
393
+ # An accent sitting near a luminance-bucket boundary can land on a biased
394
+ # candidate that is actually unreadable (e.g. a light amber `warning` just
395
+ # under the dark-mode 0.45 cutoff takes white at ~2:1). If the biased pick
396
+ # misses the bar, fall back to the higher-contrast of pure black/white.
397
+ def _ratio(fg: str) -> float:
398
+ try:
399
+ lf = relative_luminance(fg)
400
+ except Exception:
401
+ return 0.0
402
+ hi, lo = max(lf, lum), min(lf, lum)
403
+ return (hi + 0.05) / (lo + 0.05)
404
+
405
+ if _ratio(chosen) < 3.0:
406
+ chosen = "#000000" if _ratio("#000000") >= _ratio("#ffffff") else "#ffffff"
407
+ return chosen
408
+
409
+ def muted_foreground(self, background: str, min_contrast: float = 4.5) -> str:
410
+ """Return a muted foreground color with adequate contrast against background."""
411
+ return _muted_foreground(background, min_contrast)
412
+
413
+ def disabled(self, role: str = "background", surface: str | None = None) -> str:
414
+ """Return a disabled color mixed with the surface.
415
+
416
+ Args:
417
+ role: 'background' for surfaces, 'text' for foregrounds.
418
+ surface: Optional surface color to mix against. If omitted,
419
+ uses the theme background.
420
+ """
421
+ surface = surface or self.color('background')
422
+
423
+ if role == "text":
424
+ if self.provider.mode == "light":
425
+ gray = "#6c757d"
426
+ mix_ratio = 0.35
427
+ else:
428
+ gray = "#adb5bd"
429
+ mix_ratio = 0.25
430
+ elif role == "background":
431
+ if self.provider.mode == "light":
432
+ gray = "#dee2e6"
433
+ mix_ratio = 0.15
434
+ else:
435
+ gray = "#495057"
436
+ mix_ratio = 0.20
437
+ else:
438
+ raise ValueError(f"Invalid role: {role}. Expected 'text' or 'background'.")
439
+
440
+ return mix_colors(gray, surface, mix_ratio)
441
+
442
+ def elevate(self, color: str, elevation: int = 0, max_elevation: int = 5) -> str:
443
+ if elevation <= 0:
444
+ return color
445
+ blend_target = "#000000" if self.provider.mode == "light" else "#ffffff"
446
+ weight = min(elevation / max_elevation, 1.0) * 0.3
447
+ return mix_colors(blend_target, color, weight)
448
+
449
+ @staticmethod
450
+ def _state_color(color: str, state: str) -> str:
451
+ if state == "focus":
452
+ return color
453
+
454
+ delta = {
455
+ "active": 0.08,
456
+ "selected": 0.18, # was effectively ~0.10–0.16; bump a bit
457
+ "pressed": 0.12,
458
+ "focus": 0.08,
459
+ }[state]
460
+
461
+ # Selected should read as "latched": always darken.
462
+ if state == "selected":
463
+ return darken_color(color, delta)
464
+
465
+ lum = relative_luminance(color)
466
+ if lum < 0.5:
467
+ return lighten_color(color, delta)
468
+ return darken_color(color, delta)
469
+
470
+ @staticmethod
471
+ def scale(value: Union[int, List, Tuple]):
472
+ return scale_size(value)
473
+
474
+ @staticmethod
475
+ def scale_from_source(value: Union[int, float, List, Tuple]):
476
+ """Scale a value measured in source image pixels.
477
+
478
+ Source images are created at SOURCE_RESOLUTION (2x). This method
479
+ converts source pixel measurements to 1x values, then applies DPI scaling.
480
+
481
+ Use this when you measure border, padding, or other values directly
482
+ from your source image assets.
483
+
484
+ Args:
485
+ value: Pixel measurement from source image (int, float, or sequence)
486
+
487
+ Returns:
488
+ DPI-scaled value appropriate for the current display
489
+
490
+ Example:
491
+ # If your source image has 10px corners:
492
+ border=b.scale_from_source(10) # Correctly scales for all DPIs
493
+ """
494
+ if isinstance(value, (int, float)):
495
+ return scale_size(int(value / SOURCE_RESOLUTION + 0.5))
496
+ elif isinstance(value, (tuple, list)):
497
+ scaled = [int(v / SOURCE_RESOLUTION + 0.5) for v in value]
498
+ return scale_size(scaled)
499
+ return scale_size(value)
500
+
501
+ # ----- Icon Utilities -----
502
+
503
+ @staticmethod
504
+ def normalize_icon_spec(icon: str | IconSpec, default_size: int = 18) -> IconSpec:
505
+ """
506
+ If the icon is a string, then create a icon spec where the name is known and the size is set to default_size.
507
+ If the there is an icon spec, map the default size if one is not already specified in the spec.
508
+
509
+ Icon sizes are automatically scaled based on DPI settings. The default size of 20px provides
510
+ a balanced appearance, being slightly larger than the visible text (16px) but not overwhelming.
511
+ """
512
+ from bootstack._runtime.utility import scale_size
513
+
514
+ # Apply DPI scaling to default size
515
+ scaled_default = scale_size(default_size)
516
+
517
+ if isinstance(icon, str):
518
+ return dict(name=icon, size=scaled_default)
519
+ else:
520
+ # Create a copy to avoid mutating the original dict
521
+ # This prevents size from growing on each theme change
522
+ result = icon.copy()
523
+ if 'size' not in result or result.get('size') is None:
524
+ result['size'] = scaled_default
525
+ elif result.get('size'):
526
+ # Scale user-provided size too
527
+ result['size'] = scale_size(result['size'])
528
+ return result
529
+
530
+ def _resolve_icon_color(self, color: Any) -> str | None:
531
+ """Resolve an icon-spec color, turning a theme token into a hex value.
532
+
533
+ A hex value passes through; a theme token (such as `'primary'`) is
534
+ resolved against the active theme; any other string (e.g. a named color
535
+ like `'white'`) is left for the renderer to interpret.
536
+ """
537
+ if not color:
538
+ return None
539
+ try:
540
+ return self.color(color)
541
+ except Exception:
542
+ return color
543
+
544
+ def map_stateful_icons(self, icon: IconSpec, foreground_spec: Sequence[tuple]):
545
+ """
546
+ Build and return a TTK image state map for icons, using the
547
+ configured icon provider.
548
+
549
+ Parameters:
550
+ icon (IconSpec):
551
+ Base icon spec with default `name` and optional `size`/`color`.
552
+ Optional `state` provides per-state overrides where the value
553
+ can be either a string (state-specific icon name) or a dict
554
+ with `name` and/or `color`.
555
+
556
+ foreground_spec (Sequence[tuple]):
557
+ A sequence of state → foreground color items computed by the
558
+ builder, e.g.:
559
+ [
560
+ ('disabled', '#fafafa'),
561
+ ('pressed !disabled', '#63f3f3'),
562
+ ('hover !disabled', '#f2fa5d'),
563
+ ]
564
+
565
+ Behavior:
566
+ - If `icon.color` is provided at the root, use it as the default
567
+ color for all states unless a per-state override is provided.
568
+ - If `icon.color` is not provided, the icon color follows the
569
+ widget's foreground color provided by `foreground_spec`, unless
570
+ a per-state override is provided in `icon.state`.
571
+ - If a per-state override provides `name`, use that icon name for
572
+ the state; otherwise, fallback to the base icon `name`.
573
+ - Identical (name, size, color) combinations reuse the same image.
574
+
575
+ Returns:
576
+ list[tuple[str, str]]: List of (state, image_name) tuples suitable
577
+ for `ttk.Style.element_create(..., 'image', default, (state, image) ...)`.
578
+ """
579
+ # Normalize base values
580
+ base_name: str = icon.get('name') # type: ignore[assignment]
581
+ if not base_name:
582
+ # Nothing we can do without an icon name
583
+ return []
584
+
585
+ base_size: int = int(icon.get('size') or 20)
586
+ base_color: str | None = self._resolve_icon_color(icon.get('color'))
587
+
588
+ # Build per-state override lookup: {state_str: {'name':..., 'color':...}}
589
+ state_overrides: dict[str, IconStateMap] = {}
590
+ for entry in icon.get('state', []) or []:
591
+ try:
592
+ st, ov = entry # type: ignore[misc]
593
+ except Exception:
594
+ continue
595
+ if isinstance(ov, str):
596
+ state_overrides[st] = {'name': ov}
597
+ elif isinstance(ov, dict):
598
+ # Only keep known keys
599
+ override: IconStateMap = {}
600
+ if 'name' in ov and ov['name']:
601
+ override['name'] = ov['name'] # type: ignore[assignment]
602
+ if 'color' in ov and ov['color']:
603
+ override['color'] = self._resolve_icon_color(ov['color']) # type: ignore[assignment]
604
+ if override:
605
+ state_overrides[st] = override
606
+
607
+ # Obtain the provider callable (class or function). The provider itself
608
+ # is called as the icon constructor: provider(name, size, color) -> icon
609
+
610
+ def _resolve_fg(value: Any) -> str | None: # noqa: ANN401
611
+ """Extract a color string from the foreground state spec value."""
612
+ if isinstance(value, str):
613
+ return value
614
+ if isinstance(value, dict):
615
+ # Try common keys in order
616
+ for k in ('foreground', 'text', 'color'):
617
+ v = value.get(k)
618
+ if isinstance(v, str):
619
+ return v
620
+ return None
621
+
622
+ # Cache icons by (name, size, color) to avoid duplicates
623
+ cache: dict[tuple[str, int, str | None], Any] = {}
624
+
625
+ def _image_for(name: str, size: int, color: str | None):
626
+ key = (name, size, color)
627
+ if key in cache:
628
+ return cache[key]
629
+
630
+ # Special case: 'empty' creates a transparent placeholder image
631
+ if name == 'empty':
632
+ from bootstack.style.utility import create_transparent_image
633
+ img = create_transparent_image(size, size)
634
+ cache[key] = img
635
+ return img
636
+
637
+ from bootstack._core.images import _ImageService
638
+ img = _ImageService.get_icon(name, size, color or "black")
639
+ cache[key] = img
640
+ return img
641
+
642
+ state_image_specs: list[tuple[str, Any]] = []
643
+
644
+ def _match_override(expr: str) -> IconStateMap | None:
645
+ # 1) Exact expression match (e.g., 'hover !disabled')
646
+ if expr in state_overrides:
647
+ return state_overrides[expr]
648
+ # 2) Token match: treat state keywords as tokens; ignore negations like '!disabled'
649
+ tokens = {t for t in expr.split() if t and not t.startswith('!')}
650
+ for key, ov in state_overrides.items():
651
+ if not key:
652
+ # skip empty-key overrides here; apply only to '' base state
653
+ continue
654
+ if key in tokens:
655
+ return ov
656
+ # 3) No match
657
+ return None
658
+
659
+ # Build ordered list of states to map
660
+ base_states: list[str] = [s for s, _ in foreground_spec]
661
+ fg_lookup: dict[str, Any] = {s: v for s, v in foreground_spec}
662
+
663
+ # Ensure typical derivations for overrides exist (e.g., 'hover' -> 'hover !disabled')
664
+ def _derive_expr(k: str) -> str:
665
+ if not k:
666
+ return ''
667
+ if ' ' in k or '!' in k:
668
+ return k
669
+ if k in ('pressed', 'active'):
670
+ return 'pressed !disabled'
671
+ if k == 'selected':
672
+ return 'selected !disabled'
673
+ if k == 'hover':
674
+ return 'hover !disabled'
675
+ return k
676
+
677
+ extra_states: list[str] = []
678
+ seen = set(base_states)
679
+ default_present = '' in seen
680
+ if default_present:
681
+ # Exclude default for ordering; add back later
682
+ base_states_no_default = [s for s in base_states if s != '']
683
+ else:
684
+ base_states_no_default = base_states[:]
685
+
686
+ # Add override-derived states if not already present
687
+ for k in state_overrides.keys():
688
+ expr = _derive_expr(k)
689
+ if expr not in seen:
690
+ extra_states.append(expr)
691
+ seen.add(expr)
692
+
693
+ # Compose final order: base (without ''), then extras, then default ''
694
+ ordered_states = base_states_no_default + extra_states
695
+ if default_present:
696
+ ordered_states.append('')
697
+
698
+ for state_expr in ordered_states:
699
+ fg_val = fg_lookup.get(state_expr)
700
+ # Derive per-state name/color
701
+ override = _match_override(state_expr) or {}
702
+ name = override.get('name', base_name) # type: ignore[assignment]
703
+ # Determine color priority: per-state override > base_color > foreground state color
704
+ color = override.get('color') # type: ignore[assignment]
705
+ if color is None:
706
+ if base_color is not None:
707
+ color = base_color
708
+ else:
709
+ color = _resolve_fg(fg_val)
710
+
711
+ # Build or reuse the image
712
+ img_or_photo = _image_for(name, base_size, color)
713
+ if img_or_photo is not None:
714
+ state_image_specs.append((state_expr, img_or_photo))
715
+
716
+ return state_image_specs