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
bootstack/images.py ADDED
@@ -0,0 +1,563 @@
1
+ """Toolkit-free image handles and the icon factory.
2
+
3
+ This module is the public, framework-native way to work with images and icons:
4
+
5
+ - `Image` — a lightweight handle that holds image data (or a deferred icon spec)
6
+ and is only rendered for display when you hand it to a widget. You can build one
7
+ before an application exists; nothing about the underlying toolkit is exposed.
8
+ - `get_icon` — create a theme-aware `Image` from a Bootstrap Icons name. When the
9
+ color is given as a theme token, the icon re-renders automatically as the theme
10
+ changes.
11
+ - `AppIcon` — generate a platform application icon (a glyph on a filled,
12
+ rounded background) for an `App` or `Window`.
13
+
14
+ Examples:
15
+ ```python
16
+ import bootstack as bs
17
+ from bootstack.images import Image, get_icon
18
+
19
+ with bs.App() as app:
20
+ bs.Label(image=Image.open("logo.png"))
21
+ bs.Button("Save", image=get_icon("save", color="primary"))
22
+ app.run()
23
+ ```
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import atexit
29
+ import io
30
+ import math
31
+ import os
32
+ import re
33
+ import sys
34
+ import tempfile
35
+ from dataclasses import dataclass
36
+ from pathlib import Path
37
+ from typing import TYPE_CHECKING
38
+
39
+ from bootstack._core.images import _ImageService
40
+ from bootstack._runtime.utility import scale_icon_size
41
+
42
+ if TYPE_CHECKING:
43
+ from PIL.Image import Image as _PILImage
44
+ from PIL.ImageTk import PhotoImage as _Photo
45
+
46
+ __all__ = ["Image", "get_icon", "AppIcon", "list_icons"]
47
+
48
+ # A color is treated as a literal hex value only when it starts with '#';
49
+ # every other string is a theme color token resolved against the active theme.
50
+ _HEX_RE = re.compile(r"^#[0-9a-fA-F]{3,8}$")
51
+
52
+ _DEFAULT_ICON_SIZE = 20
53
+ _DEFAULT_ICON_COLOR = "foreground"
54
+
55
+ # Sizes baked into a Windows multi-resolution `.ico`. Includes the DPI-scaled
56
+ # title-bar/taskbar sizes (20, 40, ...) so Windows finds an exact match at
57
+ # 125%/150%/200% scaling instead of upscaling the nearest one (which blurs).
58
+ _APP_ICON_SIZES = [16, 20, 24, 32, 40, 48, 64, 96, 128, 256]
59
+
60
+ # With shape='auto' on Windows/Linux, frames below this size are drawn glyph-only
61
+ # (a bare mark reads cleaner than a cramped tile in a tiny title bar) and frames
62
+ # at or above it get the rounded tile (a branded icon for the taskbar, the task
63
+ # switcher, and launchers).
64
+ _AUTO_TILE_MIN_SIZE = 24
65
+ # The rounded tile is supersampled to at least this canvas size before being
66
+ # downscaled, so its curved corners get antialiased (PIL draws them aliased at
67
+ # 1x). The glyph is NOT supersampled with it — it is rendered at its final size.
68
+ _MIN_TILE_CANVAS = 128
69
+
70
+ # Fraction of the icon edge the glyph box occupies, interpolated by size. Small
71
+ # icons use less padding (a bigger glyph) so the mark stays legible when there are
72
+ # few pixels; large icons get app-icon-style margins. (The glyph renderer adds a
73
+ # little inset of its own, so the visible mark is somewhat smaller than this.)
74
+ # Endpoints are reached at <= 16 px and >= 96 px; sizes between interpolate.
75
+ _GLYPH_FRAC_SMALL_SIZE = 16
76
+ _GLYPH_FRAC_LARGE_SIZE = 96
77
+ # On a tile the glyph sits inside the rounded background; glyph-only icons (no
78
+ # tile) let the mark nearly fill the frame.
79
+ _TILE_GLYPH_FRAC = (0.80, 0.64) # (small, large)
80
+ _GLYPH_ONLY_FRAC = (0.98, 0.90)
81
+
82
+
83
+ def _glyph_frac(size: int, *, tiled: bool) -> float:
84
+ """Glyph box fraction for `size`, smaller icons getting less padding."""
85
+ small, large = _TILE_GLYPH_FRAC if tiled else _GLYPH_ONLY_FRAC
86
+ lo, hi = _GLYPH_FRAC_SMALL_SIZE, _GLYPH_FRAC_LARGE_SIZE
87
+ if size <= lo:
88
+ return small
89
+ if size >= hi:
90
+ return large
91
+ t = (size - lo) / (hi - lo)
92
+ return small + t * (large - small)
93
+
94
+
95
+ def _unlink_at_exit(path: str) -> None:
96
+ """Best-effort removal of a generated runtime icon tempfile at interpreter exit."""
97
+
98
+ def _remove() -> None:
99
+ try:
100
+ os.unlink(path)
101
+ except OSError:
102
+ pass
103
+
104
+ atexit.register(_remove)
105
+
106
+
107
+ def _resolve_color(color: str) -> str:
108
+ """Resolve a color string to a literal value.
109
+
110
+ A `#hex` value is returned unchanged; any other string is treated as a theme
111
+ color token and resolved against the active theme, falling back to the
112
+ literal string when there is no theme or the token is unknown.
113
+ """
114
+ if _HEX_RE.match(color):
115
+ return color
116
+ from bootstack.style import get_theme_color
117
+
118
+ try:
119
+ return get_theme_color(color)
120
+ except Exception:
121
+ return color
122
+
123
+
124
+ @dataclass(frozen=True)
125
+ class _IconSpec:
126
+ """A deferred icon: the parameters to render a glyph on demand."""
127
+
128
+ name: str
129
+ size: int
130
+ color: str
131
+ is_token: bool
132
+
133
+
134
+ class Image:
135
+ """A toolkit-free image handle, rendered for display only when used.
136
+
137
+ An `Image` carries a source — a file, raw bytes, an in-memory picture, or a
138
+ deferred icon — without creating any display resource up front. The picture
139
+ is rendered the moment it is handed to a widget (via `image=`), so a handle
140
+ can be built before an application is running and shared across widgets.
141
+
142
+ Build one with `Image.open`, `Image.from_bytes`, or `Image.from_pil`, or use
143
+ `get_icon` for a Bootstrap icon. Read `width` and `height` at any time.
144
+
145
+ File and byte sources accept the common raster image formats — PNG, JPEG,
146
+ GIF, BMP, TIFF, WebP, and ICO. (Animated formats load their first frame.)
147
+ """
148
+
149
+ __slots__ = ("_pil", "_path", "_data", "_icon", "_photo", "_icon_pil", "_size")
150
+
151
+ def __init__(
152
+ self,
153
+ *,
154
+ pil: "_PILImage | None" = None,
155
+ path: Path | None = None,
156
+ data: bytes | None = None,
157
+ icon: _IconSpec | None = None,
158
+ ) -> None:
159
+ self._pil = pil
160
+ self._path = path
161
+ self._data = data
162
+ self._icon = icon
163
+ self._photo: "_Photo | None" = None
164
+ self._icon_pil: "_PILImage | None" = None # cached rendered-glyph PIL
165
+ self._size: tuple[int, int] | None = None
166
+
167
+ # =========================================================================
168
+ # Constructors
169
+ # =========================================================================
170
+
171
+ @classmethod
172
+ def open(cls, path: str | Path) -> "Image":
173
+ """Create an image handle from a file on disk.
174
+
175
+ The file is decoded lazily, so the path is not read until the image is
176
+ displayed or its size is queried.
177
+
178
+ Args:
179
+ path: Path to the image file. Supports `~` expansion. Accepts the
180
+ common raster formats — PNG, JPEG, GIF, BMP, TIFF, WebP, and ICO.
181
+
182
+ Returns:
183
+ A new image handle.
184
+ """
185
+ return cls(path=Path(path).expanduser())
186
+
187
+ @classmethod
188
+ def from_bytes(cls, data: bytes) -> "Image":
189
+ """Create an image handle from raw, encoded image bytes.
190
+
191
+ Args:
192
+ data: Encoded image bytes in a common raster format — PNG, JPEG,
193
+ GIF, BMP, TIFF, WebP, or ICO — for example, an embedded resource
194
+ or a downloaded file.
195
+
196
+ Returns:
197
+ A new image handle.
198
+ """
199
+ return cls(data=bytes(data))
200
+
201
+ @classmethod
202
+ def from_pil(cls, image: "_PILImage") -> "Image":
203
+ """Create an image handle from an in-memory Pillow image.
204
+
205
+ Useful when you have already loaded or manipulated a picture with Pillow
206
+ (resizing, filtering, compositing) and want to display the result.
207
+
208
+ Args:
209
+ image: A Pillow image object to wrap.
210
+
211
+ Returns:
212
+ A new image handle.
213
+ """
214
+ return cls(pil=image)
215
+
216
+ # =========================================================================
217
+ # Size
218
+ # =========================================================================
219
+
220
+ @property
221
+ def width(self) -> int:
222
+ """The width of the image in pixels."""
223
+ return self._dimensions()[0]
224
+
225
+ @property
226
+ def height(self) -> int:
227
+ """The height of the image in pixels."""
228
+ return self._dimensions()[1]
229
+
230
+ def _dimensions(self) -> tuple[int, int]:
231
+ if self._size is None:
232
+ if self._icon is not None:
233
+ self._size = (self._icon.size, self._icon.size)
234
+ else:
235
+ pil = self._load_pil()
236
+ self._size = (pil.width, pil.height)
237
+ return self._size
238
+
239
+ def _load_pil(self) -> "_PILImage":
240
+ from PIL import Image as PILImage
241
+
242
+ if self._pil is not None:
243
+ return self._pil
244
+ if self._icon is not None:
245
+ # Render the glyph at the PIL level so icon handles work with the
246
+ # image-display widgets (Picture/Gallery) that fit/round/compose a
247
+ # PIL frame — not just the `icon=` PhotoImage path via _materialize.
248
+ # Cached so a recycling grid that re-resolves the same handle on
249
+ # every scroll doesn't re-render the glyph each time; dropped by
250
+ # _invalidate() on a theme change so token colors re-render.
251
+ if self._icon_pil is None:
252
+ size = scale_icon_size(self._icon.size)
253
+ size = size if size % 2 == 0 else size + 1
254
+ self._icon_pil = _ImageService._render_icon(
255
+ self._icon.name, size, self._resolve_icon_color()
256
+ )
257
+ return self._icon_pil
258
+ if self._path is not None:
259
+ return PILImage.open(self._path)
260
+ if self._data is not None:
261
+ return PILImage.open(io.BytesIO(self._data))
262
+ raise ValueError("image handle has no source")
263
+
264
+ # =========================================================================
265
+ # Internal rendering (called at the widget-binding boundary)
266
+ # =========================================================================
267
+
268
+ @property
269
+ def _is_theme_following(self) -> bool:
270
+ """Whether this image must re-render when the theme changes."""
271
+ return self._icon is not None and self._icon.is_token
272
+
273
+ def _invalidate(self) -> None:
274
+ """Drop the rendered image so the next bind re-renders it."""
275
+ self._photo = None
276
+ self._icon_pil = None
277
+
278
+ def _materialize(self, master=None) -> "_Photo":
279
+ """Render the source to a cached display image and return it.
280
+
281
+ The `master` is accepted for interface symmetry; the backing service
282
+ renders against the default application root, which exists by the time a
283
+ handle is bound to a widget.
284
+ """
285
+ if self._photo is not None:
286
+ return self._photo
287
+ if self._icon is not None:
288
+ color = self._resolve_icon_color()
289
+ photo = _ImageService.get_icon(self._icon.name, scale_icon_size(self._icon.size), color)
290
+ elif self._path is not None:
291
+ photo = _ImageService.open(self._path)
292
+ elif self._data is not None:
293
+ photo = _ImageService.from_bytes(self._data)
294
+ elif self._pil is not None:
295
+ photo = _ImageService.from_pil(self._pil)
296
+ else:
297
+ raise ValueError("image handle has no source")
298
+ self._photo = photo
299
+ return photo
300
+
301
+ def _resolve_icon_color(self) -> str:
302
+ spec = self._icon
303
+ assert spec is not None
304
+ if not spec.is_token:
305
+ return spec.color
306
+ return _resolve_color(spec.color)
307
+
308
+
309
+ def get_icon(
310
+ name: str,
311
+ *,
312
+ size: int = _DEFAULT_ICON_SIZE,
313
+ color: str = _DEFAULT_ICON_COLOR,
314
+ ) -> Image:
315
+ """Create a theme-aware icon image from a Bootstrap Icons name.
316
+
317
+ The returned handle can be passed to any widget that accepts `image=`. When
318
+ `color` is a theme color token (the default), the icon re-renders to match
319
+ whenever the theme changes; a literal hex color stays fixed.
320
+
321
+ Args:
322
+ name: Bootstrap Icons name, for example `'house'` or `'arrow-right'`.
323
+ Browse the catalog at https://icons.getbootstrap.com.
324
+ size: Icon size in pixels. Defaults to `20`.
325
+ color: A theme color token (such as `'foreground'`, `'primary'`, or
326
+ `'danger'`) or a literal hex string like `'#ff8800'`. Token colors
327
+ follow the active theme; hex colors are fixed. Defaults to
328
+ `'foreground'`.
329
+
330
+ Returns:
331
+ A new image handle that renders the icon on demand.
332
+ """
333
+ is_hex = bool(_HEX_RE.match(color))
334
+ spec = _IconSpec(name=name, size=int(size), color=color, is_token=not is_hex)
335
+ return Image(icon=spec)
336
+
337
+
338
+ def list_icons(contains: str | None = None) -> list[str]:
339
+ """List the available Bootstrap Icons names, sorted.
340
+
341
+ Each name can be used as a widget's `icon=` or passed to `get_icon`.
342
+
343
+ Args:
344
+ contains: If given, only names containing this text (case-insensitive)
345
+ are returned. Defaults to `None` (all names).
346
+
347
+ Returns:
348
+ A sorted list of icon names.
349
+ """
350
+ _, glyphmap = _ImageService._load_icon_assets()
351
+ names = sorted(glyphmap)
352
+ if contains:
353
+ needle = contains.lower()
354
+ names = [n for n in names if needle in n]
355
+ return names
356
+
357
+
358
+ class AppIcon:
359
+ """A generated application icon.
360
+
361
+ Renders a Bootstrap glyph in one of two shapes:
362
+
363
+ - a **tile** — the glyph in `foreground` on a filled, rounded `background`
364
+ (the macOS look); or
365
+ - **glyph-only** — the glyph alone in the `background` (brand) color on a
366
+ transparent field (the typical Windows and Linux look).
367
+
368
+ By default (`shape='auto'`) the shape follows the platform and the size: a
369
+ tile for macOS targets (`.icns`, or running on macOS); on Windows and Linux,
370
+ glyph-only at small sizes (a clean mark in a tiny title bar) and a tile at
371
+ larger sizes (a branded icon in the taskbar and launchers). A multi-size
372
+ `.ico` therefore carries both. Set `shape` to `'tile'` or `'glyph'` to force
373
+ one shape at every size.
374
+
375
+ Pass an `AppIcon` as the `icon=` of an `App` or `Window` to set its taskbar
376
+ and title-bar icon without supplying an icon file, or call `save` to export a
377
+ packaging asset (for example, an icon for a PyInstaller build).
378
+
379
+ Colors may be theme color tokens (such as `'primary'`) or literal hex
380
+ strings. Tokens are resolved once, when the icon is generated, so the icon
381
+ does not change as the theme changes; supply a token while an application is
382
+ active, or use hex values. Generating an icon outside a running application —
383
+ such as during a build — must use hex values, since no theme is available to
384
+ resolve a token against.
385
+ """
386
+
387
+ def __init__(
388
+ self,
389
+ icon: str,
390
+ *,
391
+ background: str = "primary",
392
+ foreground: str = "white",
393
+ radius: float = 0.22,
394
+ shape: str = "auto",
395
+ ) -> None:
396
+ """Describe an application icon.
397
+
398
+ Args:
399
+ icon: Bootstrap Icons name to draw, for example `'rocket'`.
400
+ background: Brand color — fills the rounded background of a tile, or
401
+ colors the glyph itself when glyph-only. A theme color token or a
402
+ hex string. Defaults to `'primary'`.
403
+ foreground: Color of the glyph on a tile (ignored when glyph-only).
404
+ A theme color token or a hex string. Defaults to `'white'`.
405
+ radius: Corner radius of the tile background as a fraction of the
406
+ icon size, from `0.0` (square) to `0.5` (circle). Ignored when
407
+ glyph-only. Defaults to `0.22`.
408
+ shape: `'auto'` (default) follows the platform and size — a tile for
409
+ macOS targets, and on Windows/Linux glyph-only at small sizes with
410
+ a tile at larger sizes. `'tile'` or `'glyph'` force one shape at
411
+ every size.
412
+ """
413
+ self._icon = icon
414
+ self._background = background
415
+ self._foreground = foreground
416
+ self._radius = max(0.0, min(0.5, float(radius)))
417
+ self._shape = shape
418
+ self._cached_path: str | None = None
419
+
420
+ def _tiled_for_size(self, size: int, *, suffix: str | None = None) -> bool:
421
+ """Decide whether to draw a tile for a given target size.
422
+
423
+ `shape='tile'`/`'glyph'` force the shape. With `shape='auto'`: macOS
424
+ targets (an `.icns` file, or — when no suffix is given — running on
425
+ macOS) are always a tile; Windows and Linux targets are glyph-only below
426
+ `_AUTO_TILE_MIN_SIZE` and a tile at or above it, so a multi-size `.ico`
427
+ carries bare marks for the title bar and tiles for the taskbar.
428
+ """
429
+ if self._shape == "tile":
430
+ return True
431
+ if self._shape == "glyph":
432
+ return False
433
+ if suffix == ".icns" or (suffix is None and sys.platform == "darwin"):
434
+ return True
435
+ return size >= _AUTO_TILE_MIN_SIZE
436
+
437
+ def _render(self, size: int, *, tiled: bool) -> "_PILImage":
438
+ from PIL import Image as PILImage, ImageDraw
439
+
440
+ bg = _resolve_color(self._background)
441
+ fg = _resolve_color(self._foreground)
442
+
443
+ glyph_frac = _glyph_frac(size, tiled=tiled)
444
+ glyph_color = fg if tiled else bg
445
+
446
+ # Render the glyph at its FINAL on-screen size. A font glyph is crispest at
447
+ # its display size — the glyph renderer grid-fits it there and sharpens
448
+ # small sizes. If the glyph were supersampled with the tile and the whole
449
+ # composite downscaled, it would lose that and go soft at 16/24 px.
450
+ glyph_box = max(1, int(size * glyph_frac))
451
+ glyph = _ImageService._render_icon(self._icon, glyph_box, glyph_color, align=True)
452
+
453
+ def _center(base: "_PILImage") -> "_PILImage":
454
+ base.alpha_composite(
455
+ glyph, ((size - glyph.width) // 2, (size - glyph.height) // 2)
456
+ )
457
+ return base
458
+
459
+ if not tiled:
460
+ # Glyph-only: the brand-colored mark on a transparent field.
461
+ return _center(PILImage.new("RGBA", (size, size), (0, 0, 0, 0)))
462
+
463
+ # Tile: supersample ONLY the rounded background — its curved corners need
464
+ # antialiasing PIL won't apply at 1x — then downscale at an integer ratio
465
+ # for clean edges, and drop the native-size glyph on top.
466
+ scale = max(2, math.ceil(_MIN_TILE_CANVAS / size))
467
+ canvas = size * scale
468
+ tile = PILImage.new("RGBA", (canvas, canvas), (0, 0, 0, 0))
469
+ ImageDraw.Draw(tile).rounded_rectangle(
470
+ [0, 0, canvas - 1, canvas - 1],
471
+ radius=int(canvas * self._radius),
472
+ fill=bg,
473
+ )
474
+ tile = tile.resize((size, size), PILImage.Resampling.LANCZOS)
475
+ return _center(tile)
476
+
477
+ def to_image(self, size: int = 256, *, tiled: bool | None = None) -> "Image":
478
+ """Render the icon to an `Image` handle for on-screen display.
479
+
480
+ Use this to show the application icon inside the app itself — for example
481
+ on an about screen or a splash panel.
482
+
483
+ Args:
484
+ size: Rendered size in pixels. Defaults to `256`.
485
+ tiled: Force the tile shape (`True`) or glyph-only (`False`). By
486
+ default, follows the icon's `shape`.
487
+
488
+ Returns:
489
+ An image handle wrapping the rendered icon.
490
+ """
491
+ size = int(size)
492
+ if tiled is None:
493
+ tiled = self._tiled_for_size(size)
494
+ return Image.from_pil(self._render(size, tiled=tiled))
495
+
496
+ def save(self, path: str | Path) -> str:
497
+ """Write the icon to a file, choosing the format by file extension.
498
+
499
+ Use this to produce a packaging asset — for example, an icon to point a
500
+ PyInstaller build at. The format follows the path suffix:
501
+
502
+ - `.ico` — a multi-resolution Windows icon.
503
+ - `.icns` — a macOS icon.
504
+ - `.png` — a single 256-pixel image.
505
+
506
+ Args:
507
+ path: Destination file path ending in `.ico`, `.icns`, or `.png`.
508
+
509
+ Returns:
510
+ The path that was written, as a string.
511
+
512
+ Raises:
513
+ ValueError: If the file extension is not one of the supported types.
514
+ """
515
+ return self._write(Path(path))
516
+
517
+ def _write(self, p: Path) -> str:
518
+ suffix = p.suffix.lower()
519
+ if suffix == ".ico":
520
+ # Each frame chooses its own shape: under `auto`, small frames are
521
+ # glyph-only and larger frames are tiled (see `_tiled_for_size`).
522
+ imgs = [
523
+ self._render(s, tiled=self._tiled_for_size(s, suffix=suffix))
524
+ for s in _APP_ICON_SIZES
525
+ ]
526
+ # Largest first — Pillow skips sizes larger than the base image.
527
+ imgs[-1].save(
528
+ p,
529
+ format="ICO",
530
+ bitmap_format="bmp",
531
+ sizes=[(s, s) for s in _APP_ICON_SIZES],
532
+ append_images=imgs[:-1],
533
+ )
534
+ elif suffix == ".icns":
535
+ self._render(1024, tiled=self._tiled_for_size(1024, suffix=suffix)).save(
536
+ p, format="ICNS"
537
+ )
538
+ elif suffix == ".png":
539
+ self._render(256, tiled=self._tiled_for_size(256, suffix=suffix)).save(
540
+ p, format="PNG"
541
+ )
542
+ else:
543
+ raise ValueError(
544
+ f"unsupported app icon format {suffix!r}; use .ico, .icns, or .png"
545
+ )
546
+ return str(p)
547
+
548
+ def _icon_path(self) -> str:
549
+ """Generate a runtime icon asset and return its file path.
550
+
551
+ On Windows a multi-resolution `.ico` is produced; elsewhere a `.png`.
552
+ The shape follows the icon's `shape` — under `auto`, a tile on macOS and,
553
+ on Windows/Linux, glyph-only for the small `.ico` frames and a tile for
554
+ the larger ones. Generated once per `AppIcon` and cached for reuse.
555
+ """
556
+ if self._cached_path is not None:
557
+ return self._cached_path
558
+ suffix = ".ico" if sys.platform == "win32" else ".png"
559
+ fd, tmp = tempfile.mkstemp(suffix=suffix, prefix="bs-appicon-")
560
+ os.close(fd)
561
+ self._cached_path = self._write(Path(tmp))
562
+ _unlink_at_exit(self._cached_path)
563
+ return self._cached_path
bootstack/py.typed ADDED
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,11 @@
1
+ """Deferred and repeating task scheduling tied to a widget's lifetime.
2
+
3
+ Every public widget exposes a `Schedule` via its `schedule` property; each
4
+ scheduling call returns a cancellable `Job`.
5
+ """
6
+ from bootstack.scheduling._schedule import Job, Schedule
7
+
8
+ __all__ = [
9
+ "Job",
10
+ "Schedule",
11
+ ]