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,381 @@
1
+ """Accordion widget - a container of mutually exclusive expanders."""
2
+ from __future__ import annotations
3
+
4
+ from typing_extensions import Unpack
5
+ from typing import Any, Callable, TYPE_CHECKING
6
+
7
+ from bootstack.events import AccordionChangeEvent
8
+ from bootstack.widgets._impl.primitives.frame import Frame, FrameKwargs
9
+ from bootstack.widgets._impl.primitives.separator import Separator
10
+ from bootstack.widgets._impl.composites.expander import Expander
11
+ from bootstack.widgets._impl.mixins import configure_delegate
12
+ from bootstack.widgets.types import Master
13
+
14
+ if TYPE_CHECKING:
15
+ pass
16
+
17
+
18
+ class Accordion(Frame):
19
+ """A container of Expander widgets with optional mutual exclusion.
20
+
21
+ The Accordion manages a group of Expanders, optionally enforcing that only
22
+ one can be expanded at a time. When `allow_multiple=False`, expanding one
23
+ section automatically collapses the others. When `allow_collapse_all=False`,
24
+ at least one section must remain open.
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ master: Master = None,
30
+ *,
31
+ allow_multiple: bool = False,
32
+ allow_collapse_all: bool = True,
33
+ show_separators: bool = False,
34
+ accent: str = None,
35
+ variant: str = None,
36
+ **kwargs: Unpack[FrameKwargs]
37
+ ):
38
+ """Create an Accordion widget.
39
+
40
+ Args:
41
+ master: Parent widget. If None, uses the default root window.
42
+ allow_multiple: If True, multiple sections can be open at once.
43
+ If False (default), only one section can be open at a time.
44
+ allow_collapse_all: If True (default), all sections can be collapsed.
45
+ If False, at least one section must remain open.
46
+ show_separators: If True, show separators between expanders.
47
+ accent: Accent token for the expanders (e.g., 'success', 'primary').
48
+ variant: Variant for the expanders (e.g., 'solid', 'default').
49
+ **kwargs: Additional arguments passed to Frame.
50
+ """
51
+
52
+ if 'show_border' in kwargs:
53
+ kwargs.setdefault('padding', 1) # required to avoid clipping corners
54
+
55
+ super().__init__(master, **kwargs)
56
+
57
+ self._allow_multiple = allow_multiple
58
+ self._allow_collapse_all = allow_collapse_all
59
+ self._show_separators = show_separators
60
+ self._accent = accent
61
+ self._variant = variant
62
+ self._expanders: dict[str, Expander] = {}
63
+ self._expander_order: list[str] = []
64
+ self._separator_widgets: list[Separator] = []
65
+ self._updating = False # Prevent recursive updates
66
+ self._counter = 0 # For auto-generating keys
67
+
68
+ def add(
69
+ self,
70
+ expander: Expander = None,
71
+ *,
72
+ key: str = None,
73
+ title: str = "",
74
+ icon: str | dict = None,
75
+ expanded: bool = None,
76
+ **kwargs
77
+ ) -> Expander:
78
+ """Add an expander to the accordion.
79
+
80
+ Args:
81
+ expander: Existing Expander to add. If None, creates one.
82
+ key: Unique identifier for the expander. Auto-generated if not provided.
83
+ title: Title for the expander header (when creating new).
84
+ icon: Icon for the expander header (when creating new).
85
+ expanded: Initial expansion state. If None, first expander
86
+ is expanded when allow_collapse_all=False, otherwise collapsed.
87
+ **kwargs: When expander is None, passed to Expander constructor.
88
+
89
+ Returns:
90
+ Expander: The added or created Expander widget.
91
+
92
+ Raises:
93
+ ValueError: If an expander with the same key already exists.
94
+ """
95
+ # Auto-generate key if not provided
96
+ if key is None:
97
+ key = f"expander_{self._counter}"
98
+ self._counter += 1
99
+
100
+ if key in self._expanders:
101
+ raise ValueError(f"An expander with the key '{key}' already exists.")
102
+
103
+ # Add separator before this expander (if not the first)
104
+ if self._show_separators and len(self._expanders) > 0:
105
+ sep = Separator(self, orient='horizontal')
106
+ sep.pack(fill='x')
107
+ self._separator_widgets.append(sep)
108
+
109
+ # Create expander if not provided
110
+ if expander is None:
111
+ # Determine initial expanded state
112
+ if expanded is None:
113
+ # If not collapsible and this is the first, expand it
114
+ if not self._allow_collapse_all and len(self._expanders) == 0:
115
+ expanded = True
116
+ else:
117
+ expanded = False
118
+
119
+ # Get accent/variant from kwargs or use accordion defaults
120
+ accent = self._accent or kwargs.pop('accent', None)
121
+ variant = self._variant or kwargs.pop('variant', None)
122
+
123
+ expander = Expander(
124
+ self,
125
+ title=title,
126
+ icon=icon,
127
+ expanded=expanded,
128
+ highlight=True,
129
+ accent=accent,
130
+ variant=variant,
131
+ **kwargs
132
+ )
133
+ else:
134
+ # Configure existing expander
135
+ expander.configure(highlight=False)
136
+ if expanded is not None:
137
+ if expanded:
138
+ expander.expand()
139
+ else:
140
+ expander.collapse()
141
+
142
+ expander.pack(fill='x')
143
+ self._expanders[key] = expander
144
+ self._expander_order.append(key)
145
+
146
+ # Bind to toggle events
147
+ expander.on_toggled(lambda e, exp=expander: self._on_expander_toggle(exp, e))
148
+
149
+ return expander
150
+
151
+ def remove(self, key: str) -> None:
152
+ """Remove an expander from the accordion.
153
+
154
+ Args:
155
+ key: The key of the expander to remove.
156
+
157
+ Raises:
158
+ KeyError: If no expander with the given key exists.
159
+
160
+ Note:
161
+ The expander widget is destroyed. If allow_collapse_all=False and
162
+ removing would leave no expanders, or would remove the only
163
+ open expander, another expander will be expanded automatically.
164
+ """
165
+ if key not in self._expanders:
166
+ raise KeyError(f"No expander with key '{key}'")
167
+
168
+ expander = self._expanders[key]
169
+ index = self._expander_order.index(key)
170
+ was_expanded = expander['expanded']
171
+
172
+ # Remove associated separator
173
+ if self._show_separators and self._separator_widgets:
174
+ if index > 0:
175
+ # Remove the separator before this expander
176
+ sep_index = index - 1
177
+ elif len(self._expanders) > 1:
178
+ # Removing first expander - remove the separator after it
179
+ sep_index = 0
180
+ else:
181
+ sep_index = None
182
+
183
+ if sep_index is not None and sep_index < len(self._separator_widgets):
184
+ sep = self._separator_widgets.pop(sep_index)
185
+ sep.destroy()
186
+
187
+ # Remove expander from dict/list and destroy
188
+ del self._expanders[key]
189
+ self._expander_order.remove(key)
190
+ expander.destroy()
191
+
192
+ # Handle allow_collapse_all=False constraint
193
+ if not self._allow_collapse_all and was_expanded and self._expanders:
194
+ # The removed expander was open - need to open another
195
+ expander_list = [self._expanders[k] for k in self._expander_order]
196
+ any_open = any(exp['expanded'] for exp in expander_list)
197
+ if not any_open:
198
+ expander_list[0].expand()
199
+
200
+ # Fire change event
201
+ if self._expanders:
202
+ expander_list = [self._expanders[k] for k in self._expander_order]
203
+ self.event_generate('<<AccordionChange>>', data=AccordionChangeEvent(
204
+ expanded=tuple(exp for exp in expander_list if exp['expanded'])
205
+ ))
206
+
207
+ def _on_expander_toggle(self, expander: Expander, event):
208
+ """Handle expander toggle events."""
209
+ if self._updating:
210
+ return
211
+
212
+ self._updating = True
213
+ try:
214
+ is_expanded = event.data.expanded
215
+ expander_list = [self._expanders[k] for k in self._expander_order]
216
+
217
+ if is_expanded:
218
+ # Expander was just opened
219
+ if not self._allow_multiple:
220
+ # Collapse all others
221
+ for exp in expander_list:
222
+ if exp is not expander and exp['expanded']:
223
+ exp.collapse()
224
+ else:
225
+ # Expander was just closed
226
+ if not self._allow_collapse_all:
227
+ # Check if any are still open
228
+ any_open = any(exp['expanded'] for exp in expander_list)
229
+ if not any_open:
230
+ # Re-open this one - can't collapse all
231
+ expander.expand()
232
+ return
233
+
234
+ # Fire change event
235
+ self.event_generate('<<AccordionChange>>', data=AccordionChangeEvent(
236
+ expanded=tuple(exp for exp in expander_list if exp['expanded'])
237
+ ))
238
+ finally:
239
+ self._updating = False
240
+
241
+ def item(self, key: str) -> Expander:
242
+ """Get an expander by its key.
243
+
244
+ Args:
245
+ key: The key of the expander to retrieve.
246
+
247
+ Returns:
248
+ The Expander instance.
249
+
250
+ Raises:
251
+ KeyError: If no expander with the given key exists.
252
+ """
253
+ if key not in self._expanders:
254
+ raise KeyError(f"No expander with key '{key}'")
255
+ return self._expanders[key]
256
+
257
+ def items(self, expanded: bool | None = None) -> tuple[Expander, ...]:
258
+ """Get expander widgets in insertion order, optionally filtered by state.
259
+
260
+ Args:
261
+ expanded: If `True`, return only expanded expanders. If `False`,
262
+ return only collapsed expanders. If `None` (default), return all.
263
+
264
+ Returns:
265
+ A tuple of matching Expander instances.
266
+ """
267
+ expanders = (self._expanders[key] for key in self._expander_order)
268
+ if expanded is None:
269
+ return tuple(expanders)
270
+ return tuple(e for e in expanders if bool(e['expanded']) == expanded)
271
+
272
+ def keys(self) -> tuple[str, ...]:
273
+ """Get all expander keys in order.
274
+
275
+ Returns:
276
+ A tuple of all expander keys in the order they were added.
277
+ """
278
+ return tuple(self._expander_order)
279
+
280
+ def configure_item(self, key: str, option: str = None, **kwargs: Any):
281
+ """Configure a specific expander by its key.
282
+
283
+ Args:
284
+ key: The key of the expander to configure.
285
+ option: If provided, return the value of this option.
286
+ **kwargs: Configuration options to apply to the expander.
287
+
288
+ Returns:
289
+ If option is provided, returns the value of that option.
290
+ """
291
+ expander = self.item(key)
292
+ if option is not None:
293
+ return expander.cget(option)
294
+ expander.configure(**kwargs)
295
+
296
+ def expand(self, key: str):
297
+ """Expand the expander with the given key.
298
+
299
+ Args:
300
+ key: Key of the expander to expand.
301
+ """
302
+ if key in self._expanders:
303
+ self._expanders[key].expand()
304
+
305
+ def collapse(self, key: str):
306
+ """Collapse the expander with the given key.
307
+
308
+ Args:
309
+ key: Key of the expander to collapse.
310
+
311
+ Note:
312
+ If allow_collapse_all=False and this is the only open expander,
313
+ this call will be ignored.
314
+ """
315
+ if key in self._expanders:
316
+ exp = self._expanders[key]
317
+ if not self._allow_collapse_all:
318
+ # Check if this is the only open one
319
+ expander_list = [self._expanders[k] for k in self._expander_order]
320
+ open_count = sum(1 for e in expander_list if e['expanded'])
321
+ if open_count <= 1 and exp['expanded']:
322
+ return # Can't collapse the last one
323
+ exp.collapse()
324
+
325
+ def expand_all(self):
326
+ """Expand all expanders (only effective when allow_multiple=True)."""
327
+ if not self._allow_multiple:
328
+ return
329
+ for exp in self._expanders.values():
330
+ exp.expand()
331
+
332
+ def collapse_all(self):
333
+ """Collapse all expanders (only effective when allow_collapse_all=True)."""
334
+ if not self._allow_collapse_all:
335
+ return
336
+ for exp in self._expanders.values():
337
+ exp.collapse()
338
+
339
+ @configure_delegate('allow_multiple')
340
+ def _delegate_allow_multiple(self, value=None):
341
+ """Get or set whether multiple sections can be open at once."""
342
+ if value is None:
343
+ return self._allow_multiple
344
+ self._allow_multiple = value
345
+ return None
346
+
347
+ @configure_delegate('allow_collapse_all')
348
+ def _delegate_allow_collapse_all(self, value=None):
349
+ """Get or set whether all sections can be collapsed."""
350
+ if value is None:
351
+ return self._allow_collapse_all
352
+ self._allow_collapse_all = value
353
+ return None
354
+
355
+ @configure_delegate('show_separators')
356
+ def _delegate_show_separators(self, value=None):
357
+ """Get or set whether separators are shown between sections."""
358
+ if value is None:
359
+ return self._show_separators
360
+ self._show_separators = value
361
+ return None
362
+
363
+ def on_accordion_changed(self, callback: Callable[[AccordionChangeEvent], None]) -> str:
364
+ """Register a callback for `<<AccordionChange>>` events.
365
+
366
+ Args:
367
+ callback: Receives an `AccordionChangeEvent` payload with attribute
368
+ `expanded` (list of currently open Expanders).
369
+
370
+ Returns:
371
+ Bind ID — pass to `off_accordion_changed()` to unsubscribe.
372
+ """
373
+ return self.bind('<<AccordionChange>>', callback, add='+')
374
+
375
+ def off_accordion_changed(self, bind_id: str | None = None) -> None:
376
+ """Unsubscribe from `<<AccordionChange>>`.
377
+
378
+ Args:
379
+ bind_id: ID returned by `on_accordion_changed()`. If None, removes all.
380
+ """
381
+ self.unbind('<<AccordionChange>>', bind_id)
@@ -0,0 +1,191 @@
1
+ """Internal Avatar composite — a small identity badge.
2
+
3
+ Shows either an image (cover-fit, clipped to the avatar shape) or initials on a
4
+ filled tile. Canvas-backed: the image path reuses the shared `_image_fit`
5
+ helpers, and initials are drawn as Tk canvas text (so they use the framework's
6
+ fonts cross-platform — PIL has no bundled text font). The public
7
+ `bootstack.Avatar` wrapper drives this.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from tkinter import Canvas, TclError
13
+ from typing import Any, Literal
14
+
15
+ from PIL import Image as PILImage, ImageDraw
16
+ from PIL.Image import Resampling
17
+ from PIL.ImageTk import PhotoImage
18
+
19
+ from bootstack.style.style import get_style
20
+ from bootstack.widgets._impl.composites._image_fit import fit_image, resolve_pil, round_corners
21
+ from bootstack.widgets._impl.primitives.frame import Frame
22
+ from bootstack.widgets.types import Master
23
+
24
+ Shape = Literal["circle", "rounded", "square"]
25
+
26
+
27
+ def _initials_from(name: str | None, initials: str | None) -> str:
28
+ """Resolve the initials to show: explicit override, else derived from `name`."""
29
+ if initials:
30
+ return initials.strip()[:2].upper()
31
+ if name:
32
+ parts = [p for p in name.split() if p]
33
+ if not parts:
34
+ return ""
35
+ if len(parts) == 1:
36
+ return parts[0][:2].upper()
37
+ return (parts[0][0] + parts[-1][0]).upper()
38
+ return ""
39
+
40
+
41
+ class Avatar(Frame):
42
+ """Canvas-backed avatar — an image or initials on a filled tile."""
43
+
44
+ def __init__(
45
+ self,
46
+ master: Master = None,
47
+ *,
48
+ source: Any = None,
49
+ name: str | None = None,
50
+ initials: str | None = None,
51
+ size: int = 40,
52
+ shape: Shape = "circle",
53
+ background: str = "primary",
54
+ foreground: str = "white",
55
+ **kwargs: Any,
56
+ ) -> None:
57
+ super().__init__(master, **kwargs)
58
+
59
+ self._size = max(8, int(size))
60
+ self._shape: Shape = shape
61
+ self._background = background
62
+ self._foreground = foreground
63
+ self._source = source
64
+ self._name = name
65
+ self._initials = initials
66
+ self._photo: PhotoImage | None = None # strong ref (Tk GC)
67
+
68
+ self._canvas = Canvas(self, width=self._size, height=self._size,
69
+ highlightthickness=0, bd=0,
70
+ background=self._surface_color())
71
+ self._canvas.pack()
72
+ self._canvas.bind("<Button-1>", self._forward_click)
73
+
74
+ self._render()
75
+
76
+ # ----- helpers -----------------------------------------------------------
77
+
78
+ def _surface_color(self) -> str:
79
+ token = getattr(self, "_surface", None) or "background"
80
+ b = get_style().style_builder
81
+ try:
82
+ return b.color(token)
83
+ except Exception:
84
+ return b.color("background")
85
+
86
+ def _color(self, token: str) -> str:
87
+ try:
88
+ return get_style().style_builder.color(token)
89
+ except Exception:
90
+ return token
91
+
92
+ def _radius(self) -> int:
93
+ if self._shape == "circle":
94
+ return self._size // 2
95
+ if self._shape == "rounded":
96
+ return max(1, int(self._size * 0.25))
97
+ return 0
98
+
99
+ # ----- rendering ---------------------------------------------------------
100
+
101
+ def _render(self) -> None:
102
+ self._canvas.delete("all")
103
+ pil = resolve_pil(self._source) if self._source is not None else None
104
+ if pil is not None:
105
+ self._render_image(pil)
106
+ else:
107
+ self._render_initials()
108
+
109
+ def _render_image(self, pil: PILImage.Image) -> None:
110
+ composed = fit_image(pil.convert("RGBA"), self._size, self._size, "cover")
111
+ radius = self._radius()
112
+ if radius > 0:
113
+ composed = round_corners(composed, radius)
114
+ self._photo = PhotoImage(composed)
115
+ self._canvas.create_image(self._size // 2, self._size // 2,
116
+ image=self._photo, anchor="center")
117
+
118
+ def _render_initials(self) -> None:
119
+ # Filled tile (antialiased) in the background color, clipped to the shape.
120
+ ss = 4
121
+ s = self._size
122
+ big = PILImage.new("RGBA", (s * ss, s * ss), (0, 0, 0, 0))
123
+ fill = _hex_or_name_to_rgba(self._color(self._background))
124
+ ImageDraw.Draw(big).rounded_rectangle(
125
+ [0, 0, s * ss - 1, s * ss - 1], radius=self._radius() * ss, fill=fill
126
+ )
127
+ self._photo = PhotoImage(big.resize((s, s), Resampling.LANCZOS))
128
+ self._canvas.create_image(s // 2, s // 2, image=self._photo, anchor="center")
129
+
130
+ text = _initials_from(self._name, self._initials)
131
+ if text:
132
+ font_size = max(8, int(s * 0.4))
133
+ self._canvas.create_text(
134
+ s // 2, s // 2, text=text, fill=self._text_color(),
135
+ font=(self._ui_family(), font_size, "bold"), anchor="center",
136
+ )
137
+
138
+ def _text_color(self) -> str:
139
+ """The resolved foreground color, validated as a Tk color (else white)."""
140
+ color = self._color(self._foreground)
141
+ try:
142
+ self._canvas.winfo_rgb(color) # authoritative Tk color parse
143
+ return color
144
+ except TclError:
145
+ return "#ffffff"
146
+
147
+ @staticmethod
148
+ def _ui_family() -> str:
149
+ """The framework's configured UI font family.
150
+
151
+ bootstack overrides `TkDefaultFont` to its body font, so reading its
152
+ actual family gives the app's UI font for the initials text.
153
+ """
154
+ from tkinter import font as tkfont
155
+
156
+ try:
157
+ return tkfont.nametofont("TkDefaultFont").actual("family")
158
+ except Exception:
159
+ return ""
160
+
161
+ def _bs_apply_theme(self, _event=None) -> None:
162
+ try:
163
+ self._canvas.configure(background=self._surface_color())
164
+ except TclError:
165
+ return
166
+ self._render()
167
+
168
+ def _forward_click(self, event) -> None:
169
+ self.event_generate("<<AvatarClick>>", x=event.x, y=event.y)
170
+
171
+ # ----- live updates ------------------------------------------------------
172
+
173
+ def set_source(self, source: Any) -> None:
174
+ self._source = source
175
+ self._render()
176
+
177
+ def set_text(self, *, name: str | None = None, initials: str | None = None) -> None:
178
+ self._name = name
179
+ self._initials = initials
180
+ self._render()
181
+
182
+
183
+ def _hex_or_name_to_rgba(color: str) -> tuple[int, int, int, int]:
184
+ """Parse a resolved color (hex or name) to an opaque RGBA tuple for PIL fill."""
185
+ from PIL import ImageColor
186
+
187
+ try:
188
+ r, g, b = ImageColor.getrgb(color)[:3]
189
+ return (r, g, b, 255)
190
+ except (ValueError, TypeError):
191
+ return (108, 117, 125, 255) # neutral gray fallback