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,463 @@
1
+ """Image loading and caching utilities for bootstack.
2
+
3
+ This module provides a unified image service for loading, caching, and managing
4
+ Tk-compatible PhotoImage objects. It uses Pillow as the backend to support a wide
5
+ variety of image formats beyond what Tk natively supports.
6
+
7
+ The primary interface is the `_ImageService` class, which provides class methods
8
+ for loading images from various sources while automatically caching them to avoid
9
+ repeated decoding and to prevent garbage collection issues with Tk images.
10
+
11
+ This is the internal render/cache backend. The public, Tk-free image handle that
12
+ delegates to it lives in `bootstack.images` (`Image`/`get_icon`/`AppIcon`).
13
+
14
+ Examples:
15
+ Basic usage with file paths:
16
+
17
+ ```python
18
+ from bootstack._core.images import _ImageService
19
+
20
+ # Load an image from disk (cached automatically)
21
+ photo = _ImageService.open("icons/save.png")
22
+ button = Button(app, image=photo)
23
+ ```
24
+
25
+ Loading from bytes (e.g., embedded resources):
26
+
27
+ ```python
28
+ photo = _ImageService.from_bytes(icon_data)
29
+ ```
30
+
31
+ Creating transparent spacer images:
32
+
33
+ ```python
34
+ spacer = _ImageService.transparent(16, 16)
35
+ ```
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ import hashlib
41
+ import io
42
+ import json
43
+ from dataclasses import dataclass
44
+ from pathlib import Path
45
+ from typing import Any, Hashable
46
+
47
+ from PIL import Image as PILImage, ImageDraw, ImageFilter, ImageFont
48
+ from PIL.ImageTk import PhotoImage as PILPhotoImage
49
+
50
+ _ICONS_DIR = Path(__file__).parent.parent / "assets" / "icons"
51
+ _ICON_Y_BIAS = 0.02
52
+ _ICON_PAD_FACTOR = 0.10
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class ImageCacheInfo:
57
+ """Information about the current state of an image cache.
58
+
59
+ Attributes:
60
+ items: The number of images currently stored in the cache.
61
+ """
62
+
63
+ items: int
64
+
65
+
66
+ class _ImageService:
67
+ """Platform image service for loading and caching PhotoImage objects.
68
+
69
+ This class provides a centralized service for working with images in
70
+ bootstack applications. It handles:
71
+
72
+ - Loading images from files, bytes, or PIL Image objects
73
+ - Automatic caching to avoid repeated decoding
74
+ - Keeping strong references to prevent Tk garbage collection issues
75
+ - Support for many image formats via Pillow (PNG, JPEG, GIF, BMP, etc.)
76
+
77
+ All methods are class methods, so no instantiation is required.
78
+
79
+ Examples:
80
+ ```python
81
+ # From a file path
82
+ icon = _ImageService.open("path/to/icon.png")
83
+
84
+ # From raw bytes
85
+ icon = _ImageService.from_bytes(embedded_data)
86
+
87
+ # From a PIL Image object
88
+ pil_img = PILImage.open("photo.jpg").resize((100, 100))
89
+ icon = _ImageService.from_pil(pil_img)
90
+
91
+ # Create a transparent spacer
92
+ spacer = _ImageService.transparent(32, 32)
93
+ ```
94
+
95
+ Note:
96
+ Images are cached by default using automatically generated keys based
97
+ on the source (file path, content hash, or object id). You can provide
98
+ a custom `key` parameter to control caching behavior.
99
+ """
100
+
101
+ _cache: dict[Hashable, PILPhotoImage] = {}
102
+ _icon_font_bytes: bytes | None = None
103
+ _icon_glyphmap: dict[str, str] | None = None
104
+ _icon_metrics: dict[str, list[float]] | None = None
105
+ _icon_font_cache: dict[int, Any] = {}
106
+
107
+ # =========================================================================
108
+ # Cache Management
109
+ # =========================================================================
110
+
111
+ @classmethod
112
+ def get_cached(cls, key: Hashable) -> PILPhotoImage | None:
113
+ """Retrieve a cached PhotoImage by its key.
114
+
115
+ Args:
116
+ key: The cache key to look up.
117
+
118
+ Returns:
119
+ The cached PhotoImage if found, or None if not in cache.
120
+ """
121
+ return cls._cache.get(key)
122
+
123
+ @classmethod
124
+ def set_cached(cls, key: Hashable, img: PILPhotoImage) -> PILPhotoImage:
125
+ """Store a PhotoImage in the cache.
126
+
127
+ Args:
128
+ key: The cache key to store the image under.
129
+ img: The PhotoImage to cache.
130
+
131
+ Returns:
132
+ The same PhotoImage that was passed in (for chaining).
133
+ """
134
+ cls._cache[key] = img
135
+ return img
136
+
137
+ @classmethod
138
+ def clear_cache(cls) -> None:
139
+ """Clear all cached images.
140
+
141
+ This removes all images from the cache. Use with caution, as any
142
+ widgets still referencing these images may display incorrectly.
143
+ """
144
+ cls._cache.clear()
145
+
146
+ @classmethod
147
+ def cache_info(cls) -> ImageCacheInfo:
148
+ """Get information about the current cache state.
149
+
150
+ Returns:
151
+ An ImageCacheInfo object containing cache statistics.
152
+
153
+ Examples:
154
+ >>> info = _ImageService.cache_info()
155
+ >>> print(f"Cached images: {info.items}")
156
+ """
157
+ return ImageCacheInfo(items=len(cls._cache))
158
+
159
+ # =========================================================================
160
+ # Image Constructors
161
+ # =========================================================================
162
+
163
+ @classmethod
164
+ def open(cls, path: str | Path, *, key: Hashable | None = None) -> PILPhotoImage:
165
+ """Load an image from a file path.
166
+
167
+ Opens an image file using Pillow and converts it to a Tk-compatible
168
+ PhotoImage. The result is cached to avoid repeated disk reads and
169
+ decoding for the same file.
170
+
171
+ Args:
172
+ path: Path to the image file. Supports ~ expansion and relative paths.
173
+ key: Optional custom cache key. If not provided, the absolute file
174
+ path is used as the cache key.
175
+
176
+ Returns:
177
+ A Tk-compatible PhotoImage that can be used with widgets.
178
+
179
+ Examples:
180
+ >>> photo = _ImageService.open("assets/logo.png")
181
+ >>> label = Label(app, image=photo)
182
+
183
+ >>> # With custom cache key for versioning
184
+ >>> photo = _ImageService.open("icon.png", key=("icon", "v2"))
185
+ """
186
+ p = Path(path).expanduser().resolve()
187
+ cache_key = key if key is not None else ("file", str(p))
188
+ cached = cls.get_cached(cache_key)
189
+ if cached is not None:
190
+ return cached
191
+
192
+ pil = PILImage.open(p)
193
+ photo = PILPhotoImage(image=pil)
194
+ return cls.set_cached(cache_key, photo)
195
+
196
+ @classmethod
197
+ def from_pil(cls, image: PILImage.Image, *, key: Hashable | None = None) -> PILPhotoImage:
198
+ """Convert a PIL Image to a Tk PhotoImage.
199
+
200
+ Wraps an existing PIL Image object in a Tk-compatible PhotoImage.
201
+ This is useful when you need to perform image manipulation (resizing,
202
+ filtering, etc.) before displaying the image.
203
+
204
+ Args:
205
+ image: A PIL Image object to convert.
206
+ key: Optional custom cache key. If not provided, a key derived from
207
+ the image's content is used, so equal images share a photo and a
208
+ new image can never collide with a freed one's cached photo.
209
+
210
+ Returns:
211
+ A Tk-compatible PhotoImage that can be used with widgets.
212
+
213
+ Examples:
214
+ >>> from PIL import Image as PILImage
215
+ >>> pil_img = PILImage.open("photo.jpg")
216
+ >>> pil_img = pil_img.resize((100, 100))
217
+ >>> photo = _ImageService.from_pil(pil_img)
218
+ """
219
+ if key is not None:
220
+ cache_key: Hashable = key
221
+ else:
222
+ # Content-based key. id(image) is NOT safe: PIL images are short-lived
223
+ # (e.g. a freshly rendered icon), and once garbage-collected their id
224
+ # can be reused by a different image — which would then be served the
225
+ # previous image's cached photo (a stale/duplicated picture).
226
+ try:
227
+ cache_key = (
228
+ "pil", image.mode, image.size,
229
+ hashlib.md5(image.tobytes()).hexdigest(),
230
+ )
231
+ except Exception:
232
+ # Content not byte-serializable — render fresh, uncached.
233
+ return PILPhotoImage(image=image)
234
+ cached = cls.get_cached(cache_key)
235
+ if cached is not None:
236
+ return cached
237
+
238
+ photo = PILPhotoImage(image=image)
239
+ return cls.set_cached(cache_key, photo)
240
+
241
+ @classmethod
242
+ def from_bytes(cls, data: bytes, *, key: Hashable | None = None) -> PILPhotoImage:
243
+ """Create a PhotoImage from raw image bytes.
244
+
245
+ Decodes image data from bytes using Pillow and converts it to a
246
+ Tk-compatible PhotoImage. This is useful for embedded resources,
247
+ downloaded images, or any other source of raw image data.
248
+
249
+ Args:
250
+ data: Raw image bytes in any format supported by Pillow
251
+ (PNG, JPEG, GIF, BMP, etc.).
252
+ key: Optional custom cache key. If not provided, an MD5 hash
253
+ of the bytes is used as the cache key.
254
+
255
+ Returns:
256
+ A Tk-compatible PhotoImage that can be used with widgets.
257
+
258
+ Examples:
259
+ >>> # Load from embedded resource
260
+ >>> with open("icon.png", "rb") as f:
261
+ ... icon_data = f.read()
262
+ >>> photo = _ImageService.from_bytes(icon_data)
263
+
264
+ >>> # With custom cache key
265
+ >>> photo = _ImageService.from_bytes(data, key="my-icon")
266
+ """
267
+ digest = hashlib.md5(data).hexdigest()
268
+ cache_key = key if key is not None else ("bytes", digest)
269
+ cached = cls.get_cached(cache_key)
270
+ if cached is not None:
271
+ return cached
272
+
273
+ pil = PILImage.open(io.BytesIO(data))
274
+ photo = PILPhotoImage(image=pil)
275
+ return cls.set_cached(cache_key, photo)
276
+
277
+ @classmethod
278
+ def transparent(cls, width: int, height: int, *, key: Hashable | None = None) -> PILPhotoImage:
279
+ """Create a transparent spacer image.
280
+
281
+ Creates a fully transparent RGBA image of the specified dimensions.
282
+ This is useful for creating spacing in layouts or as a placeholder
283
+ image.
284
+
285
+ Args:
286
+ width: Width of the image in pixels.
287
+ height: Height of the image in pixels.
288
+ key: Optional custom cache key. If not provided, a tuple of
289
+ ("transparent", width, height) is used.
290
+
291
+ Returns:
292
+ A transparent Tk-compatible PhotoImage.
293
+
294
+ Examples:
295
+ >>> # Create a 16x16 transparent spacer
296
+ >>> spacer = _ImageService.transparent(16, 16)
297
+ >>> label = Label(app, image=spacer)
298
+
299
+ >>> # Use as compound image padding
300
+ >>> button = Button(app, image=spacer, compound="left")
301
+ """
302
+ cache_key = key if key is not None else ("transparent", width, height)
303
+ cached = cls.get_cached(cache_key)
304
+ if cached is not None:
305
+ return cached
306
+
307
+ pil = PILImage.new("RGBA", (width, height), (255, 255, 255, 0))
308
+ photo = PILPhotoImage(image=pil)
309
+ return cls.set_cached(cache_key, photo)
310
+
311
+ # =========================================================================
312
+ # Icon Rendering
313
+ # =========================================================================
314
+
315
+ @classmethod
316
+ def _load_icon_assets(cls) -> tuple[bytes, dict[str, str]]:
317
+ if cls._icon_font_bytes is not None:
318
+ return cls._icon_font_bytes, cls._icon_glyphmap # type: ignore[return-value]
319
+ cls._icon_font_bytes = (_ICONS_DIR / "bootstrap.ttf").read_bytes()
320
+ raw: dict[str, int] = json.loads((_ICONS_DIR / "glyphmap.json").read_text(encoding="utf-8"))
321
+ cls._icon_glyphmap = {name: chr(cp) for name, cp in raw.items()}
322
+ return cls._icon_font_bytes, cls._icon_glyphmap
323
+
324
+ @classmethod
325
+ def _get_icon_metrics(cls) -> dict[str, list[float]]:
326
+ """Lazy-load precomputed normalized ink bounds per glyph.
327
+
328
+ Maps icon name to `[left, top, width, height]` as fractions of the font
329
+ size (from the text draw origin), generated by
330
+ `tools/generate_icon_metrics.py`. Missing file degrades gracefully to an
331
+ empty map (the renderer then falls back to measuring at render time).
332
+ """
333
+ if cls._icon_metrics is None:
334
+ try:
335
+ cls._icon_metrics = json.loads((_ICONS_DIR / "icon_metrics.json").read_text(encoding="utf-8"))
336
+ except (OSError, ValueError):
337
+ cls._icon_metrics = {}
338
+ return cls._icon_metrics
339
+
340
+ @classmethod
341
+ def _get_icon_font(cls, size: int) -> ImageFont.FreeTypeFont:
342
+ font = cls._icon_font_cache.get(size)
343
+ if font is None:
344
+ font_bytes, _ = cls._load_icon_assets()
345
+ font = ImageFont.truetype(io.BytesIO(font_bytes), size)
346
+ cls._icon_font_cache[size] = font
347
+ return font
348
+
349
+ @classmethod
350
+ def get_icon(cls, name: str, size: int, color: str) -> PILPhotoImage:
351
+ """Render a Bootstrap icon and return a cached Tk PhotoImage.
352
+
353
+ Even-pixel snapping is applied before rendering to eliminate half-pixel
354
+ LANCZOS blur at non-integer DPI scale factors (e.g., 125%, 150%).
355
+
356
+ Args:
357
+ name: Bootstrap icon name (e.g., `"house"`, `"arrow-right-fill"`).
358
+ size: Requested pixel size. Snapped to the nearest even integer.
359
+ color: Foreground color as a hex string (e.g., `"#ffffff"`).
360
+
361
+ Returns:
362
+ A cached Tk-compatible PhotoImage for the icon.
363
+ """
364
+ size = size if size % 2 == 0 else size + 1
365
+ key = ("icon", name, size, color)
366
+ cached = cls.get_cached(key)
367
+ if cached is not None:
368
+ return cached
369
+ pil = cls._render_icon(name, size, color)
370
+ photo = PILPhotoImage(image=pil)
371
+ return cls.set_cached(key, photo)
372
+
373
+ @classmethod
374
+ def _render_icon(
375
+ cls, name: str, size: int, color: str, *, align: bool = False
376
+ ) -> PILImage.Image:
377
+ """Render a named glyph to an RGBA image of `size` x `size`.
378
+
379
+ Args:
380
+ name: Bootstrap Icons glyph name.
381
+ size: Output edge length in pixels.
382
+ color: Fill color (hex).
383
+ align: Snap the glyph's draw origin to the final pixel grid. Crisper
384
+ for standalone marks (e.g. app icons) where the glyph fills the
385
+ frame; off by default so glyphs sitting beside text keep their
386
+ exact optical centering.
387
+ """
388
+ _, glyphmap = cls._load_icon_assets()
389
+ glyph = glyphmap.get(name)
390
+ if glyph is None:
391
+ return PILImage.new("RGBA", (size, size), (0, 0, 0, 0))
392
+
393
+ if size < 32:
394
+ oversample = 3
395
+ elif size < 64:
396
+ oversample = 2
397
+ else:
398
+ oversample = 1
399
+
400
+ def _snap(dx: float, dy: float) -> tuple[float, float]:
401
+ # Round the draw origin to a multiple of `oversample` so that after
402
+ # the downscale the glyph lands on whole pixels — its edges stay crisp
403
+ # instead of being antialiased across a sub-pixel offset.
404
+ if not align:
405
+ return dx, dy
406
+ return round(dx / oversample) * oversample, round(dy / oversample) * oversample
407
+
408
+ canvas_size = size * oversample
409
+ pad = int(canvas_size * _ICON_PAD_FACTOR)
410
+ inner = canvas_size - 2 * pad
411
+
412
+ img = PILImage.new("RGBA", (canvas_size, canvas_size), (0, 0, 0, 0))
413
+ draw = ImageDraw.Draw(img)
414
+
415
+ metrics = cls._get_icon_metrics().get(name)
416
+ if metrics:
417
+ # Precomputed normalized ink bounds (em fractions, from the text
418
+ # draw origin): [left, top, width, height]. Fit the ink to the inner
419
+ # box (never enlarging past natural size) and center on the true ink
420
+ # — accurate for full-bleed glyphs and free of getbbox skew.
421
+ nl, nt, nw, nh = metrics
422
+ font_size = max(1, int(min(canvas_size, inner / max(nw, nh, 1e-6))))
423
+ font = cls._get_icon_font(font_size)
424
+ ink_w, ink_h = nw * font_size, nh * font_size
425
+ dx = (canvas_size - ink_w) / 2 - nl * font_size
426
+ dy = (canvas_size - ink_h) / 2 - nt * font_size
427
+ if _ICON_Y_BIAS:
428
+ dy += canvas_size * _ICON_Y_BIAS
429
+ dx, dy = _snap(dx, dy)
430
+ draw.text((dx, dy), glyph, font=font, fill=color)
431
+ else:
432
+ # Fallback for glyphs absent from icon_metrics.json: measure with
433
+ # getbbox at render time (under-reports full-bleed ink, but always
434
+ # works). Regenerate the metrics with tools/generate_icon_metrics.py.
435
+ eff_size = max(1, canvas_size)
436
+ font = cls._get_icon_font(eff_size)
437
+ ascent, descent = font.getmetrics()
438
+ bbox = font.getbbox(glyph)
439
+ glyph_w = bbox[2] - bbox[0]
440
+ glyph_h = bbox[3] - bbox[1]
441
+
442
+ if glyph_w > inner or glyph_h > inner:
443
+ scale_factor = min(inner / max(glyph_w, 1), inner / max(glyph_h, 1)) * 0.95
444
+ scaled_size = max(1, int(eff_size * scale_factor))
445
+ font = cls._get_icon_font(scaled_size)
446
+ ascent, descent = font.getmetrics()
447
+ bbox = font.getbbox(glyph)
448
+ glyph_w = bbox[2] - bbox[0]
449
+ glyph_h = bbox[3] - bbox[1]
450
+
451
+ full_height = ascent + descent
452
+ dx = pad + (inner - glyph_w) // 2 - bbox[0]
453
+ dy = pad + (inner - full_height) // 2 + (ascent - bbox[3])
454
+ if _ICON_Y_BIAS:
455
+ dy += int(canvas_size * _ICON_Y_BIAS)
456
+ dx, dy = _snap(dx, dy)
457
+ draw.text((dx, dy), glyph, font=font, fill=color)
458
+
459
+ if oversample > 1:
460
+ img = img.resize((size, size), PILImage.Resampling.LANCZOS)
461
+ img = img.filter(ImageFilter.UnsharpMask(radius=0.6, percent=60, threshold=0))
462
+
463
+ return img
@@ -0,0 +1 @@
1
+ """Widget capability and TTK state mixins for bootstack widgets."""
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Iterable, Any
4
+
5
+
6
+ class TtkStateMixin:
7
+ """ttk-only widget helpers.
8
+
9
+ This mixin contains methods that exist on ttk widgets but not on classic Tk widgets
10
+ (e.g., `tk.Text`, `tk.Canvas`).
11
+ """
12
+
13
+ def state(self, statespec: str | Iterable[str] | None = None) -> Any:
14
+ """Get or modify the ttk state of the widget.
15
+
16
+ Args:
17
+ statespec: State specification (e.g. ("disabled",) or ("!disabled",)).
18
+ If None, returns the current state.
19
+
20
+ Returns:
21
+ The current state (getter) or an implementation-dependent result.
22
+ """
23
+ return super().state(statespec) # type: ignore[misc]
24
+
25
+ def instate(self, statespec: str | Iterable[str], callback: Any = None) -> bool:
26
+ """Test the ttk state of the widget.
27
+
28
+ Args:
29
+ statespec: State specification to test.
30
+ callback: Optional callable invoked if the test succeeds.
31
+
32
+ Returns:
33
+ True if the widget matches the state spec; otherwise False.
34
+ """
35
+ return super().instate(statespec, callback) # type: ignore[misc]
@@ -0,0 +1,132 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Mapping, Any
4
+
5
+ from bootstack._core.capabilities.after import AfterMixin
6
+ from bootstack._core.capabilities.bind import BindingsMixin
7
+ from bootstack._core.capabilities.bindtags import BindtagsMixin
8
+ from bootstack._core.capabilities.clipboard import ClipboardMixin
9
+ from bootstack._core.capabilities.focus import FocusMixin
10
+ from bootstack._core.capabilities.grab import GrabMixin
11
+ from bootstack._core.capabilities.grid import GridMixin
12
+ from bootstack._core.capabilities.pack import PackMixin
13
+ from bootstack._core.capabilities.place import PlaceMixin
14
+ from bootstack._core.capabilities.selection import SelectionMixin
15
+ from bootstack._core.capabilities.winfo import WinfoMixin
16
+
17
+
18
+ class WidgetCapabilitiesMixin(
19
+ AfterMixin,
20
+ BindingsMixin,
21
+ BindtagsMixin,
22
+ ClipboardMixin,
23
+ FocusMixin,
24
+ GrabMixin,
25
+ GridMixin,
26
+ PackMixin,
27
+ PlaceMixin,
28
+ SelectionMixin,
29
+ WinfoMixin,
30
+ ):
31
+ """Common widget API surface (tk + ttk).
32
+
33
+ This mixin aggregates documented capability mixins into a single interface
34
+ suitable for both Tk and ttk widgets, plus a small set of commonly used
35
+ widget operations (configure/cget/destroy, stacking order, etc.).
36
+ """
37
+
38
+ # -------------------------------------------------------------------------
39
+ # Lifecycle
40
+ # -------------------------------------------------------------------------
41
+
42
+ def destroy(self) -> None:
43
+ """Destroy this widget and release its Tk resources."""
44
+ return super().destroy() # type: ignore[misc]
45
+
46
+ # -------------------------------------------------------------------------
47
+ # Configuration
48
+ # -------------------------------------------------------------------------
49
+
50
+ def configure(self, cnf: Mapping[str, Any] | None = None, **kw: Any) -> Any:
51
+ """Configure widget options.
52
+
53
+ Args:
54
+ cnf: Optional mapping of option values.
55
+ **kw: Option values as keyword arguments.
56
+
57
+ Returns:
58
+ Tk returns configuration details when called with no args; otherwise
59
+ the return value is implementation-dependent.
60
+ """
61
+ if cnf is None:
62
+ return super().configure(**kw) # type: ignore[misc]
63
+ return super().configure(cnf, **kw) # type: ignore[misc]
64
+
65
+ config = configure
66
+
67
+ def cget(self, key: str) -> Any:
68
+ """Return the current value for an option.
69
+
70
+ Args:
71
+ key: Option name (with or without a leading dash).
72
+
73
+ Returns:
74
+ The option value.
75
+ """
76
+ return super().cget(key) # type: ignore[misc]
77
+
78
+ # -------------------------------------------------------------------------
79
+ # Stacking order
80
+ # -------------------------------------------------------------------------
81
+
82
+ def lift(self, aboveThis: Any | None = None) -> None:
83
+ """Raise this widget above its siblings.
84
+
85
+ Args:
86
+ aboveThis: Optional sibling widget to raise above.
87
+ """
88
+ return super().lift(aboveThis) # type: ignore[misc]
89
+
90
+ tkraise = lift
91
+
92
+ def lower(self, belowThis: Any | None = None) -> None:
93
+ """Lower this widget below its siblings.
94
+
95
+ Args:
96
+ belowThis: Optional sibling widget to lower below.
97
+ """
98
+ return super().lower(belowThis) # type: ignore[misc]
99
+
100
+ # -------------------------------------------------------------------------
101
+ # Layout convenience
102
+ # -------------------------------------------------------------------------
103
+
104
+ def forget(self) -> None:
105
+ """Remove the widget from layout (best-effort).
106
+
107
+ This calls the appropriate forget method depending on the active geometry manager:
108
+
109
+ - grid -> grid_forget()
110
+ - pack -> pack_forget()
111
+ - place -> place_forget()
112
+ """
113
+ try:
114
+ mgr = self.winfo_manager() # type: ignore[attr-defined]
115
+ except Exception:
116
+ mgr = ""
117
+
118
+ if mgr == "grid":
119
+ try:
120
+ self.grid_forget() # type: ignore[attr-defined]
121
+ except Exception:
122
+ pass
123
+ elif mgr == "pack":
124
+ try:
125
+ self.pack_forget() # type: ignore[attr-defined]
126
+ except Exception:
127
+ pass
128
+ elif mgr == "place":
129
+ try:
130
+ self.place_forget() # type: ignore[attr-defined]
131
+ except Exception:
132
+ pass
@@ -0,0 +1,49 @@
1
+ """Platform configuration paths for bootstack apps.
2
+
3
+ A single home for the per-platform config directory convention so window-state
4
+ persistence, `Store`, and anything else that writes user state all agree on
5
+ where files live.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ __all__ = ["user_config_dir", "app_config_dir", "app_config_file"]
15
+
16
+
17
+ def user_config_dir() -> Path:
18
+ """Return the OS user-configuration base directory.
19
+
20
+ `~/Library/Application Support` on macOS, `%APPDATA%` on Windows, and
21
+ `$XDG_CONFIG_HOME` (falling back to `~/.config`) elsewhere.
22
+ """
23
+ if sys.platform == "darwin":
24
+ return Path.home() / "Library" / "Application Support"
25
+ if sys.platform == "win32":
26
+ return Path(os.environ.get("APPDATA") or (Path.home() / "AppData" / "Roaming"))
27
+ return Path(os.environ.get("XDG_CONFIG_HOME") or (Path.home() / ".config"))
28
+
29
+
30
+ def app_config_dir(app_name: str | None = None) -> Path:
31
+ """Return the per-app config directory under `user_config_dir()`.
32
+
33
+ The leaf is `app_name` (or `'bootstack'` when not given) so multiple
34
+ bootstack apps on one machine do not collide.
35
+
36
+ Args:
37
+ app_name: The application name, used as the directory leaf.
38
+ """
39
+ return user_config_dir() / (app_name or "bootstack")
40
+
41
+
42
+ def app_config_file(filename: str, app_name: str | None = None) -> Path:
43
+ """Return the path to a named file inside the per-app config directory.
44
+
45
+ Args:
46
+ filename: The leaf file name (for example `'settings.json'`).
47
+ app_name: The application name, used as the directory leaf.
48
+ """
49
+ return app_config_dir(app_name) / filename