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,406 @@
1
+ from tkinter import Event, TclError
2
+ from typing import Any, Callable
3
+
4
+ from bootstack.events import ChangeEvent, InputEvent
5
+ from bootstack.i18n import MessageCatalog, IntlFormatter
6
+ from bootstack.widgets._impl.primitives.entry import Entry
7
+ from bootstack.widgets._impl.mixins import ValidationMixin
8
+ from bootstack.widgets._impl.mixins.configure_mixin import configure_delegate
9
+
10
+
11
+ class TextEntryPart(ValidationMixin, Entry):
12
+ """Internationalization-aware entry widget with deferred parsing and formatting.
13
+
14
+ This widget separates user input (display text) from the committed/parsed value,
15
+ providing a clean pattern for handling formatted data entry. Parsing and formatting
16
+ only occur when the user commits the value via `<FocusOut>` or `<Return>`.
17
+
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ master=None,
23
+ *,
24
+ value='',
25
+ value_format=None,
26
+ initial_focus: bool = False,
27
+ allow_blank: bool = True,
28
+ placeholder: str | None = None,
29
+ **kwargs
30
+ ):
31
+ """Initialize a TextEntryPart widget with internationalization support.
32
+
33
+ Creates an entry widget that separates display text from the parsed value.
34
+ The widget defers parsing and formatting until the user commits the value
35
+ by pressing Enter or moving focus away from the widget.
36
+
37
+ Args:
38
+ master: Parent widget. If None, uses the default root window.
39
+ value: Initial value to display and parse. Can be a string or any value
40
+ that can be formatted using value_format. Default is empty string.
41
+ value_format: ICU format pattern for parsing and formatting the value.
42
+ Common patterns: `'#,##0.00'` (decimal), `'¤#,##0.00'` (currency),
43
+ `'yyyy-MM-dd'` (date), `'#,##0.00%'` (percent).
44
+ If None, value is treated as plain text (no parsing/formatting).
45
+ initial_focus: If True, widget receives focus when created.
46
+ allow_blank: If True, empty input is parsed as None. If False, empty
47
+ input preserves the previous value.
48
+ placeholder: Text shown when the field is empty and unfocused.
49
+ **kwargs: Additional keyword arguments passed to the Entry base class.
50
+
51
+ Note:
52
+ The widget automatically subscribes to text changes and sets up
53
+ event handlers for `<FocusIn>`, `<FocusOut>`, and `<Return>`.
54
+ """
55
+ self._placeholder_text = placeholder
56
+ self._showing_placeholder = False
57
+ kwargs.setdefault('ttk_class', 'TField')
58
+ kwargs.setdefault('variant', 'input')
59
+ super().__init__(master, **kwargs)
60
+
61
+ # configuration
62
+ self._value_format = value_format
63
+ self._allow_blank = allow_blank
64
+ self._on_input_fid = None
65
+ self._fmt = IntlFormatter(locale=MessageCatalog.locale())
66
+
67
+ # set the initial display value
68
+ # Convert to string if it's a number
69
+ if isinstance(value, (int, float)):
70
+ initial_display = str(value)
71
+ else:
72
+ initial_display = value or self.textsignal() or ''
73
+
74
+ # Parse initial value if format is specified
75
+ if value_format is not None:
76
+ initial_value = self._parse_or_none(initial_display)
77
+ else:
78
+ initial_value = initial_display or ''
79
+
80
+ self._value = initial_value
81
+ self._prev_changed_value = initial_value
82
+
83
+ # normalize initial display if we already have a parsed value
84
+ if self._value is not None:
85
+ formatted_text = self._format_value(self._value)
86
+ self.textsignal.set(formatted_text)
87
+ else:
88
+ self.textsignal.set('')
89
+
90
+ # track last text emitted for CHANGE
91
+ self._prev_change_text = self.textsignal()
92
+
93
+ # subscribe to text changes
94
+ self._on_input_fid = self.textsignal.subscribe(self._handle_change)
95
+
96
+ # Commit on focus out / enter;
97
+ self.bind('<FocusIn>', self._store_prev_value, add=True)
98
+ self.bind('<FocusOut>', self._handle_focus_out, add=True)
99
+ self.bind('<Return>', self._handle_return, add=True)
100
+ self.winfo_toplevel().bind('<<LocaleChanged>>', self._on_locale_changed, add='+')
101
+
102
+ # set initial focus
103
+ if initial_focus:
104
+ self.focus()
105
+
106
+ # placeholder — shown when empty and unfocused; uses textvariable detach
107
+ # so the Signal is never set to the placeholder text
108
+ if placeholder:
109
+ self._default_fg = self.cget("foreground")
110
+ # remember any masking char so the placeholder can render unmasked
111
+ # (a password '•'/'*' mask must not turn the placeholder into bullets)
112
+ self._show_char = self.cget("show")
113
+ if not self.textsignal():
114
+ self._show_placeholder()
115
+ self.bind('<FocusIn>', self._on_focus_in_placeholder, add=True)
116
+ self.bind('<FocusOut>', self._on_focus_out_placeholder, add=True)
117
+
118
+ def _show_placeholder(self) -> None:
119
+ self._showing_placeholder = True
120
+ self.configure(textvariable='')
121
+ # render the placeholder unmasked even when an input mask is active
122
+ if getattr(self, '_show_char', ''):
123
+ self.configure(show='')
124
+ self.insert(0, self._placeholder_text)
125
+ self.configure(foreground='grey')
126
+
127
+ def _hide_placeholder(self) -> None:
128
+ self._showing_placeholder = False
129
+ self.delete(0, 'end')
130
+ self.configure(textvariable=self.textsignal.var)
131
+ # restore the input mask now that real input is shown
132
+ if getattr(self, '_show_char', ''):
133
+ self.configure(show=self._show_char)
134
+ self.configure(foreground=self._default_fg)
135
+
136
+ def _on_focus_in_placeholder(self, _event) -> None:
137
+ if self._showing_placeholder:
138
+ self._hide_placeholder()
139
+
140
+ def _on_focus_out_placeholder(self, _event) -> None:
141
+ if self._placeholder_text and not self.textsignal():
142
+ self._show_placeholder()
143
+
144
+ def _store_prev_value(self, _: Any):
145
+ """Store current value on focus-in to detect changes later."""
146
+ self._prev_changed_value = self._value
147
+
148
+ def _handle_focus_out(self, _):
149
+ """Commit value and check for changes when focus leaves the widget."""
150
+ if 'readonly' in self.state():
151
+ return
152
+ self.commit()
153
+ self._check_if_changed()
154
+
155
+ def _handle_return(self, _):
156
+ """Commit value and check for changes when Return key is pressed."""
157
+ self.commit()
158
+ self._check_if_changed()
159
+
160
+ def _handle_change(self, event):
161
+ """Emit <<Input>> event on every text change without parsing."""
162
+ text = self.textsignal()
163
+ if text == self._prev_change_text:
164
+ return
165
+ if self._showing_placeholder:
166
+ if text:
167
+ self._hide_placeholder()
168
+ else:
169
+ return
170
+ self._prev_change_text = text
171
+ self.event_generate('<<Input>>', data=InputEvent(text=text))
172
+
173
+ def _check_if_changed(self):
174
+ """Emit <<Change>> event if parsed value changed since focus-in."""
175
+ if self._value != self._prev_changed_value:
176
+ data = ChangeEvent(
177
+ value=self._value,
178
+ prev_value=self._prev_changed_value,
179
+ text=self.textsignal(),
180
+ )
181
+ self.event_generate('<<Change>>', data=data)
182
+ self._prev_changed_value = self._value
183
+
184
+ def _parse_or_none(self, s: str):
185
+ """Parse string using value_format, returning None on empty/invalid input."""
186
+ # If a non-string is passed (e.g., datetime/date), assume it's already parsed.
187
+ if not isinstance(s, str):
188
+ return s
189
+ s2 = (s or '').strip()
190
+ if not s2:
191
+ return None
192
+ try:
193
+ if self._value_format is None:
194
+ return s2
195
+ return self._fmt.parse(s2, self._value_format)
196
+ except ValueError:
197
+ return None
198
+
199
+ def _format_value(self, value: Any) -> str:
200
+ """Format a value for display using value_format.
201
+
202
+ Args:
203
+ value: The value to format (can be string, int, float, etc.)
204
+
205
+ Returns:
206
+ Formatted string representation of the value
207
+ """
208
+ if value is None:
209
+ return ''
210
+
211
+ # If no format specified, just convert to string
212
+ if self._value_format is None:
213
+ return str(value)
214
+
215
+ # IntlFormatter.format() expects numeric values, not strings
216
+ # So pass the value directly (it will be a number from parse())
217
+ try:
218
+ return self._fmt.format(value, self._value_format)
219
+ except (ValueError, TypeError):
220
+ # If formatting fails, return string representation
221
+ return str(value)
222
+
223
+ def on_input(self, callback: Callable) -> str:
224
+ """Bind to `<<Input>>`. Callback receives `event.data = {'text': str}`."""
225
+ return self.bind('<<Input>>', callback, add=True)
226
+
227
+ def off_input(self, bind_id: str | None = None) -> None:
228
+ """Unbind from `<<Input>>`."""
229
+ self.unbind('<<Input>>', bind_id)
230
+
231
+ def on_enter(self, callback: Callable) -> str:
232
+ """Bind to `<Return>`. Callback receives `event.data = {'value': Any, 'text': str}`."""
233
+
234
+ def enrich_callback(event: Event) -> None:
235
+ data = {"value": self._value, "text": self.textsignal()}
236
+ event.data = data
237
+ return callback(event)
238
+
239
+ return self.bind('<Return>', enrich_callback, add=True)
240
+
241
+ def off_enter(self, bind_id: str | None = None) -> None:
242
+ """Unbind from `<Return>`."""
243
+ self.unbind('<Return>', bind_id)
244
+
245
+ def on_changed(self, callback: Callable) -> str:
246
+ """Bind to `<<Change>>`. Callback receives `event.data = {'value': Any, 'prev_value': Any, 'text': str}`."""
247
+ return self.bind("<<Change>>", callback, add=True)
248
+
249
+ def off_changed(self, bind_id: str | None = None) -> None:
250
+ """Unbind from `<<Change>>`."""
251
+ self.unbind('<<Change>>', bind_id)
252
+
253
+ def value(self, value=None):
254
+ """Get or set the parsed/committed value.
255
+
256
+ Args:
257
+ value: If provided, sets the display text and internal value with formatting
258
+
259
+ Returns:
260
+ Current parsed value if no argument provided, None otherwise
261
+ """
262
+ if value is None:
263
+ return self._value
264
+ else:
265
+ # Store the value and format it for display
266
+ if isinstance(value, (int, float)):
267
+ value_str = str(value)
268
+ else:
269
+ value_str = str(value) if value is not None else ''
270
+
271
+ # Parse the value if format is specified
272
+ if self._value_format is not None and value_str:
273
+ self._value = self._parse_or_none(value_str)
274
+ else:
275
+ self._value = value_str if value_str else None
276
+
277
+ # Format and display
278
+ formatted_text = self._format_value(self._value)
279
+ self.textsignal.set(formatted_text)
280
+ return None
281
+
282
+ def _get_validation_value(self):
283
+ """Return the current input parsed to its typed value, for validation.
284
+
285
+ Validation runs against the field's typed value — the string itself for
286
+ a text field, a `date`/`time` for a date or time field — parsed from
287
+ what is currently shown, not the raw display text and not the
288
+ last-committed value. This keeps a manual `validate()`, the automatic
289
+ blur/key validation, and a form-level validate in agreement on the same
290
+ live, typed input.
291
+ """
292
+ try:
293
+ return self._parse_or_none(self.get())
294
+ except Exception:
295
+ return self.get()
296
+
297
+ def text(self, value=None):
298
+ """Get or set the raw display text without committing.
299
+
300
+ Args:
301
+ value: If provided, sets the display text without parsing
302
+
303
+ Returns:
304
+ Current display text if no argument provided, None otherwise
305
+ """
306
+ if value is None:
307
+ return self.textsignal()
308
+ else:
309
+ self.textsignal.set(value)
310
+ return None
311
+
312
+ def commit(self):
313
+ """Parse display text, update value, and normalize display (called on FocusOut/Return)."""
314
+ if self._showing_placeholder:
315
+ return
316
+ s = self.get().strip()
317
+
318
+ # parse once
319
+ if s == '':
320
+ self._value = None if self._allow_blank else self._value
321
+ else:
322
+ try:
323
+ self._value = s if self._value_format is None else self._fmt.parse(s, self._value_format)
324
+ except ValueError:
325
+ # keep prior value on parse failure
326
+ return
327
+
328
+ # Format the value for display
329
+ new_text = self._format_value(self._value)
330
+
331
+ if new_text != self.textsignal():
332
+ # temporarily silence CHANGE while normalizing text
333
+ fid = getattr(self, '_on_input_fid', None)
334
+ if fid:
335
+ try:
336
+ fid.cancel()
337
+ except TclError:
338
+ pass
339
+ self.textsignal.set(new_text)
340
+ if fid:
341
+ self._on_input_fid = self.textsignal.subscribe(self._handle_change)
342
+
343
+ def _on_locale_changed(self, event=None):
344
+ """Respond to global changes in locale by updating formatter and text"""
345
+ self._fmt = IntlFormatter(locale=MessageCatalog.locale())
346
+
347
+ # reformat current value with new locale + same value_format
348
+ if self._value is not None:
349
+ formatted_text = self._format_value(self._value)
350
+ self.textsignal.set(formatted_text)
351
+
352
+ @configure_delegate('text')
353
+ def _delegate_text(self, value=None):
354
+ if value is None:
355
+ return self.text()
356
+ else:
357
+ return self.text(value)
358
+
359
+ @configure_delegate('value')
360
+ def _delegate_value(self, value=None):
361
+ if value is None:
362
+ return self.value()
363
+ else:
364
+ return self.value(value)
365
+
366
+ @configure_delegate('value_format')
367
+ def _delegate_value_format(self, value: str):
368
+ """Get or set the value format pattern and reformat display.
369
+
370
+ Can be accessed via:
371
+ widget.configure(value_format='#,##0.00')
372
+ widget['value_format'] = '#,##0.00'
373
+ widget.cget('value_format')
374
+ """
375
+ if value is None:
376
+ return self._value_format
377
+
378
+ self._value_format = value
379
+ # Reformat current value with new format
380
+ if self._value is not None:
381
+ formatted_text = self._format_value(self._value)
382
+ self.textsignal.set(formatted_text)
383
+
384
+ @configure_delegate('allow_blank')
385
+ def _delegate_allow_blank(self, value: bool):
386
+ """Get or set whether blank input is allowed.
387
+
388
+ Can be accessed via:
389
+ widget.configure(allow_blank=True)
390
+ widget['allow_blank'] = True
391
+ widget.cget('allow_blank')
392
+ """
393
+ if value is None:
394
+ return self._allow_blank
395
+
396
+ self._allow_blank = bool(value)
397
+
398
+ def destroy(self):
399
+ """Clean up signal subscriptions and destroy the widget."""
400
+ if self._on_input_fid:
401
+ try:
402
+ self._on_input_fid.cancel()
403
+ except Exception:
404
+ pass # Ignore all errors during cleanup
405
+ self._on_input_fid = None
406
+ super().destroy()
@@ -0,0 +1,33 @@
1
+ """Composite bootstack widgets."""
2
+
3
+ from bootstack.widgets._impl.composites.accordion import Accordion
4
+ from bootstack.widgets._impl.composites.compositeframe import Composite, CompositeFrame
5
+ from bootstack.widgets._impl.composites.expander import Expander
6
+ from bootstack.widgets._impl.composites.list import ListItem, ListView, MemoryDataSource, DataSourceProtocol
7
+ from bootstack.widgets._impl.composites.sidenav import (
8
+ SideNavHeader,
9
+ SideNavSeparator,
10
+ )
11
+ from bootstack.widgets._impl.composites.tabs import Tabs, TabView
12
+ from bootstack.widgets._impl.composites.selectbox import SelectBox
13
+ from bootstack.widgets._impl.composites.slider import Slider, RangeSlider
14
+ from bootstack.widgets._impl.composites.toolbar import Toolbar
15
+
16
+ __all__ = [
17
+ 'Accordion',
18
+ 'Composite',
19
+ 'CompositeFrame',
20
+ 'Expander',
21
+ 'ListItem',
22
+ 'ListView',
23
+ 'MemoryDataSource',
24
+ 'DataSourceProtocol',
25
+ 'RangeSlider',
26
+ 'SideNavHeader',
27
+ 'SideNavSeparator',
28
+ 'Slider',
29
+ 'Tabs',
30
+ 'TabView',
31
+ 'SelectBox',
32
+ 'Toolbar',
33
+ ]
@@ -0,0 +1,33 @@
1
+ """Shared date helpers for the date-picker composites."""
2
+ from __future__ import annotations
3
+
4
+ from datetime import date, datetime
5
+
6
+
7
+ def coerce_date(value: date | datetime | str | None) -> date | None:
8
+ """Coerce a date, datetime, or date string to a plain `date`, or `None`.
9
+
10
+ Accepts `date`/`datetime` instances (a `datetime` is narrowed to its
11
+ `date`), `'YYYY-MM-DD'` or `'MM/DD/YYYY'` strings, and any ISO-8601 string.
12
+ Returns `None` for `None` or anything unparseable.
13
+
14
+ Args:
15
+ value: The value to coerce.
16
+ """
17
+ if value is None:
18
+ return None
19
+ if isinstance(value, datetime):
20
+ return value.date()
21
+ if isinstance(value, date):
22
+ return value
23
+ if isinstance(value, str):
24
+ for fmt in ("%Y-%m-%d", "%m/%d/%Y"):
25
+ try:
26
+ return datetime.strptime(value, fmt).date()
27
+ except Exception:
28
+ continue
29
+ try:
30
+ return datetime.fromisoformat(value).date()
31
+ except Exception:
32
+ return None
33
+ return None
@@ -0,0 +1,105 @@
1
+ """Shared image fit/crop/round helpers for the media widgets.
2
+
3
+ Pure functions used by both `Picture` and the `Gallery` tile so the
4
+ object-fit scaling and antialiased rounded-corner masking live in one place.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+ from typing import Any, Literal
11
+
12
+ from PIL import Image as PILImage, ImageChops, ImageDraw
13
+ from PIL.Image import Resampling
14
+
15
+ FitMode = Literal["contain", "cover", "fill", "none", "scale-down"]
16
+
17
+
18
+ def resolve_pil(source: Any) -> "PILImage.Image | None":
19
+ """Resolve an image source (Image handle, path, or PIL image) to a PIL image.
20
+
21
+ Returns the first frame for an animated source, or None when `source` is
22
+ None or cannot be decoded.
23
+ """
24
+ if source is None:
25
+ return None
26
+ from bootstack.images import Image as _ImageHandle
27
+
28
+ try:
29
+ if isinstance(source, _ImageHandle):
30
+ return source._load_pil()
31
+ if isinstance(source, (str, Path)):
32
+ return PILImage.open(Path(source).expanduser())
33
+ if isinstance(source, PILImage.Image):
34
+ return source
35
+ except Exception:
36
+ return None
37
+ return None
38
+
39
+
40
+ def fit_image(frame: PILImage.Image, bw: int, bh: int, fit: FitMode) -> PILImage.Image:
41
+ """Scale (and for `cover`, crop) `frame` to a `bw`x`bh` box per `fit`.
42
+
43
+ For `contain`/`scale-down`/`none` the result may be smaller than the box —
44
+ the caller positions it. For `cover`/`fill` the result is exactly the box.
45
+ """
46
+ iw, ih = frame.size
47
+ if iw <= 0 or ih <= 0:
48
+ return frame
49
+
50
+ if fit == "fill":
51
+ return frame.resize((bw, bh), Resampling.LANCZOS)
52
+
53
+ if fit == "cover":
54
+ scale = max(bw / iw, bh / ih)
55
+ scaled = frame.resize(
56
+ (max(1, round(iw * scale)), max(1, round(ih * scale))), Resampling.LANCZOS
57
+ )
58
+ left = (scaled.width - bw) // 2
59
+ top = (scaled.height - bh) // 2
60
+ return scaled.crop((left, top, left + bw, top + bh))
61
+
62
+ # contain / scale-down / none — never crop.
63
+ if fit == "none":
64
+ scale = 1.0
65
+ else:
66
+ scale = min(bw / iw, bh / ih)
67
+ if fit == "scale-down":
68
+ scale = min(scale, 1.0)
69
+ tw, th = max(1, round(iw * scale)), max(1, round(ih * scale))
70
+ if (tw, th) == (iw, ih):
71
+ return frame.copy()
72
+ return frame.resize((tw, th), Resampling.LANCZOS)
73
+
74
+
75
+ def rounded_mask(w: int, h: int, radius: int) -> PILImage.Image:
76
+ """An antialiased rounded-rectangle alpha mask for a `w`x`h` box.
77
+
78
+ Drawn supersampled and downscaled with LANCZOS — `ImageDraw.rounded_rectangle`
79
+ is aliased at 1x, so the curve would otherwise be jagged. Callers should cache
80
+ the result; it depends only on `(w, h, radius)`.
81
+ """
82
+ radius = min(radius, w // 2, h // 2)
83
+ ss = 4 if max(w, h) <= 600 else 2
84
+ big = PILImage.new("L", (w * ss, h * ss), 0)
85
+ ImageDraw.Draw(big).rounded_rectangle(
86
+ [0, 0, w * ss - 1, h * ss - 1], radius=radius * ss, fill=255
87
+ )
88
+ return big.resize((w, h), Resampling.LANCZOS)
89
+
90
+
91
+ def round_corners(
92
+ img: PILImage.Image, radius: int, mask: PILImage.Image | None = None
93
+ ) -> PILImage.Image:
94
+ """Apply a rounded-rectangle alpha mask to `img` (intersecting existing alpha).
95
+
96
+ Pass a precomputed `mask` (from `rounded_mask`) to avoid rebuilding it.
97
+ """
98
+ w, h = img.size
99
+ if radius <= 0:
100
+ return img
101
+ if mask is None:
102
+ mask = rounded_mask(w, h, radius)
103
+ out = img.convert("RGBA")
104
+ out.putalpha(ImageChops.multiply(mask, out.getchannel("A")))
105
+ return out