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,567 @@
1
+ """Internal Carousel composite — a one-at-a-time image stepper.
2
+
3
+ A canvas-backed stage that shows a single slide from an ordered collection, with
4
+ overlaid navigation chevrons, a slide indicator, an optional caption, autoplay,
5
+ and slide/fade transitions. The public `bootstack.Carousel` wrapper drives this.
6
+
7
+ The stage is its OWN canvas (built like Picture's internal, on the shared
8
+ `_image_fit` helpers) rather than an embedded Picture widget — that is what makes
9
+ the transitions possible: a slide animates two image items via `canvas.move`, and
10
+ a fade blends two composed images frame by frame. Chevrons and dots are drawn from
11
+ the Bootstrap icon font through the icon renderer.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from tkinter import Canvas, TclError
17
+ from typing import Any, Literal
18
+
19
+ from PIL import Image as PILImage, ImageOps
20
+ from PIL.ImageTk import PhotoImage
21
+
22
+ from bootstack._core.images import _ImageService
23
+ from bootstack.data import MemoryDataSource, DataSourceProtocol
24
+ from bootstack.scheduling import Schedule
25
+ from bootstack.style.style import get_style
26
+ from bootstack.widgets._impl.composites._image_fit import (
27
+ FitMode, fit_image, resolve_pil, rounded_mask,
28
+ )
29
+ from bootstack.widgets._impl.primitives.frame import Frame
30
+ from bootstack.widgets.types import Master
31
+
32
+ Transition = Literal["slide", "fade", "none"]
33
+ Indicator = Literal["dots", "count", "none"]
34
+
35
+ _NAV_SIZE = 34 # chevron glyph size
36
+ _NAV_MARGIN = 14 # inset of a chevron from the stage edge
37
+ _DOT_SIZE = 13
38
+ _DOT_GAP = 6
39
+ _DOTS_MAX = 8 # above this, 'dots' auto-switches to a 'count' label
40
+ _CAPTION_INSET = 14 # caption baseline inset from the bottom
41
+ _FRAME_MS = 16
42
+ # Nominal stage width (px) behind the aspect-ratio height floor: with no explicit
43
+ # `height`, the minimum stage height is `_FLOOR_STAGE_WIDTH / aspect_ratio`
44
+ # (e.g. 400 / 1.5 ≈ 267). A floor, not a cap — `fill`/`expand` grow it past this,
45
+ # and a larger `aspect_ratio` (wider slide) yields a shorter floor.
46
+ _FLOOR_STAGE_WIDTH = 400
47
+
48
+
49
+ def _hex_rgb(color: str) -> tuple[int, int, int]:
50
+ """Parse a `#rgb`/`#rrggbb` color to an (r, g, b) tuple."""
51
+ c = color.lstrip("#")
52
+ if len(c) == 3:
53
+ c = "".join(ch * 2 for ch in c)
54
+ try:
55
+ return int(c[0:2], 16), int(c[2:4], 16), int(c[4:6], 16)
56
+ except (ValueError, IndexError):
57
+ return (255, 255, 255)
58
+
59
+
60
+ class Carousel(Frame):
61
+ """Canvas-backed single-slide stepper with transitions and nav chrome."""
62
+
63
+ def __init__(
64
+ self,
65
+ master: Master = None,
66
+ *,
67
+ items: list | None = None,
68
+ datasource: DataSourceProtocol | None = None,
69
+ image_field: str = "image",
70
+ caption_field: str | None = None,
71
+ index: int = 0,
72
+ fit: FitMode = "contain",
73
+ transition: Transition = "slide",
74
+ show_arrows: bool = True,
75
+ indicator: Indicator = "dots",
76
+ autoplay: bool = False,
77
+ interval: int = 4000,
78
+ loop: bool = True,
79
+ corner_radius: int = 0,
80
+ aspect_ratio: float = 1.5,
81
+ height: int | None = None,
82
+ accent: str | None = None,
83
+ **kwargs: Any,
84
+ ) -> None:
85
+ super().__init__(master, **kwargs)
86
+
87
+ self._image_field = image_field
88
+ self._caption_field = caption_field
89
+ self._fit: FitMode = fit
90
+ self._transition: Transition = transition
91
+ self._show_arrows = show_arrows
92
+ self._indicator: Indicator = indicator
93
+ self._loop = loop
94
+ self._radius = corner_radius
95
+ # None -> neutral white dots (the carousel default); a token brands the
96
+ # active dot with that accent color.
97
+ self._accent = accent
98
+ self._interval = max(250, int(interval))
99
+
100
+ if datasource is not None:
101
+ self._datasource = datasource
102
+ elif items:
103
+ self._datasource = MemoryDataSource(page_size=10_000).load(items)
104
+ else:
105
+ self._datasource = MemoryDataSource(page_size=10_000).load([])
106
+
107
+ n = self._count()
108
+ self._index = max(0, min(index, n - 1)) if n else 0
109
+ self._box = (0, 0)
110
+ self._cur_pil: PILImage.Image | None = None
111
+ self._photo: PhotoImage | None = None
112
+ self._img_id: int | None = None
113
+ self._frame_id: int | None = None # rounded corner-mask overlay
114
+ self._frame_photo: PhotoImage | None = None
115
+ self._frame_key: tuple | None = None
116
+ self._transitioning = False
117
+ self._schedule = Schedule(self)
118
+
119
+ self._autoplay = bool(autoplay)
120
+ self._autoplay_job = None
121
+ self._first_render = True
122
+
123
+ # Strong refs to overlay PhotoImages (Tk GC) — keyed so they survive redraws.
124
+ self._nav_photos: dict[str, PhotoImage] = {}
125
+ self._dot_photos: dict[str, PhotoImage] = {}
126
+ self._fade_photo: PhotoImage | None = None
127
+ self._next_photo: PhotoImage | None = None
128
+
129
+ surface = self._surface_color()
130
+ # Requested-height FLOOR so the carousel never collapses in a container
131
+ # that doesn't impose a height (a scroll view, an auto-fit window). The
132
+ # public wrapper freezes this frame with `pack_propagate(False)`, so its
133
+ # own configured `height` is its requested height; `fill`/`expand` still
134
+ # grow it past this floor when the parent has room.
135
+ floor_h = height if height else round(_FLOOR_STAGE_WIDTH / max(0.1, aspect_ratio))
136
+ self.configure(height=floor_h)
137
+ self._stage = Canvas(self, highlightthickness=0, bd=0, background=surface)
138
+ self._stage.pack(fill="both", expand=True)
139
+ self._stage.bind("<Configure>", self._on_configure, add="+")
140
+ self._stage.bind("<Button-1>", self._on_stage_click, add="+")
141
+ self._stage.bind("<Left>", lambda e: self.previous())
142
+ self._stage.bind("<Right>", lambda e: self.next())
143
+
144
+ # Auto-refresh when the data source changes externally (parity with the
145
+ # other record-native widgets); the subscription is released on destroy.
146
+ self._change_sub = None
147
+ on_change = getattr(self._datasource, "on_change", None)
148
+ if callable(on_change):
149
+ try:
150
+ self._change_sub = on_change(self._on_source_change)
151
+ except Exception:
152
+ self._change_sub = None
153
+ self.bind("<Destroy>", self._on_destroy, add="+")
154
+
155
+ # ----- theme / data ------------------------------------------------------
156
+
157
+ def _on_source_change(self, _event: Any = None) -> None:
158
+ try:
159
+ self.reload()
160
+ except Exception:
161
+ pass
162
+
163
+ def _on_destroy(self, event: Any = None) -> None:
164
+ if event is not None and getattr(event, "widget", None) is not self:
165
+ return
166
+ sub, self._change_sub = self._change_sub, None
167
+ if sub is not None:
168
+ try:
169
+ sub.cancel()
170
+ except Exception:
171
+ pass
172
+
173
+ def reload(self) -> None:
174
+ """Reload from the data source and refresh the current slide."""
175
+ n = self._count()
176
+ self._index = max(0, min(self._index, n - 1)) if n else 0
177
+ if self._box[0] > 1 and self._box[1] > 1:
178
+ self._set_current(self._compose(self._pil_at(self._index), *self._box))
179
+ self._ensure_frame(*self._box)
180
+ self._draw_overlays(*self._box)
181
+
182
+ def _surface_color(self) -> str:
183
+ token = getattr(self, "_surface", None) or "background"
184
+ b = get_style().style_builder
185
+ try:
186
+ return b.color(token)
187
+ except Exception:
188
+ return b.color("background")
189
+
190
+ def _bs_apply_theme(self, _event=None) -> None:
191
+ try:
192
+ self._stage.configure(background=self._surface_color())
193
+ except TclError:
194
+ return
195
+ self._nav_photos.clear()
196
+ self._dot_photos.clear()
197
+ self._frame_key = None # surface color changed — rebuild the corner mask
198
+ if self._box[0] > 1:
199
+ self._ensure_frame(*self._box)
200
+ self._draw_overlays(*self._box)
201
+
202
+ def _count(self) -> int:
203
+ try:
204
+ return int(self._datasource.count)
205
+ except Exception:
206
+ return 0
207
+
208
+ def _raw_at(self, i: int) -> dict | None:
209
+ try:
210
+ rows = self._datasource.page_slice(i, 1)
211
+ except Exception:
212
+ return None
213
+ return rows[0] if rows else None
214
+
215
+ def _public_at(self, i: int) -> dict | None:
216
+ raw = self._raw_at(i)
217
+ if raw is None:
218
+ return None
219
+ pub = getattr(self._datasource, "_public_record", None)
220
+ return pub(raw) if callable(pub) else dict(raw)
221
+
222
+ def _pil_at(self, i: int) -> "PILImage.Image | None":
223
+ raw = self._raw_at(i)
224
+ return resolve_pil(raw.get(self._image_field)) if raw else None
225
+
226
+ def _caption_at(self, i: int) -> str:
227
+ raw = self._raw_at(i)
228
+ if raw is None or not self._caption_field:
229
+ return ""
230
+ return str(raw.get(self._caption_field, ""))
231
+
232
+ # ----- compositing -------------------------------------------------------
233
+
234
+ def _compose(self, pil: "PILImage.Image | None", w: int, h: int) -> PILImage.Image:
235
+ """Fit `pil` into a full `w`x`h` RGBA frame (centered; transparent margin).
236
+
237
+ Corners are NOT rounded here — `corner_radius` rounds the whole stage via
238
+ a fixed corner-mask overlay, so transitions slide under a steady frame.
239
+ """
240
+ base = PILImage.new("RGBA", (w, h), (0, 0, 0, 0))
241
+ if pil is None:
242
+ return base
243
+ fitted = fit_image(pil.convert("RGBA"), w, h, self._fit)
244
+ base.alpha_composite(fitted, ((w - fitted.width) // 2, (h - fitted.height) // 2))
245
+ return base
246
+
247
+ def _set_current(self, pil_box: PILImage.Image) -> None:
248
+ self._cur_pil = pil_box
249
+ self._photo = PhotoImage(pil_box)
250
+ if self._img_id is None:
251
+ self._img_id = self._stage.create_image(0, 0, anchor="nw", image=self._photo)
252
+ else:
253
+ self._stage.itemconfigure(self._img_id, image=self._photo)
254
+ self._stage.coords(self._img_id, 0, 0)
255
+ self._stage.tag_lower(self._img_id)
256
+
257
+ def _ensure_frame(self, w: int, h: int) -> None:
258
+ """Create/update the rounded corner-mask that rounds the whole stage.
259
+
260
+ The mask paints the four corner nooks in the surface color (antialiased),
261
+ so the viewport reads as a rounded card while slides move underneath it.
262
+ """
263
+ if self._radius <= 0:
264
+ if self._frame_id is not None:
265
+ self._stage.delete(self._frame_id)
266
+ self._frame_id = None
267
+ return
268
+ key = (w, h, self._radius, self._surface_color())
269
+ if self._frame_key != key or self._frame_photo is None:
270
+ rgb = _hex_rgb(self._surface_color())
271
+ cover = PILImage.new("RGBA", (w, h), rgb + (255,))
272
+ inside = rounded_mask(w, h, self._radius) # 255 inside (AA edge)
273
+ cover.putalpha(ImageOps.invert(inside)) # opaque only in the corners
274
+ self._frame_photo = PhotoImage(cover)
275
+ self._frame_key = key
276
+ if self._frame_id is None:
277
+ self._frame_id = self._stage.create_image(
278
+ 0, 0, anchor="nw", image=self._frame_photo, tags="frame"
279
+ )
280
+ else:
281
+ self._stage.itemconfigure(self._frame_id, image=self._frame_photo)
282
+ self._stage.coords(self._frame_id, 0, 0)
283
+ self._stage.tag_raise(self._frame_id)
284
+
285
+ # ----- layout / render ---------------------------------------------------
286
+
287
+ def _on_configure(self, event) -> None:
288
+ box = (int(event.width), int(event.height))
289
+ if box == self._box:
290
+ return
291
+ self._box = box
292
+ if box[0] <= 1 or box[1] <= 1:
293
+ return
294
+ # Don't fight an in-flight transition; it recomposes at the settled box.
295
+ if self._transitioning:
296
+ return
297
+ self._set_current(self._compose(self._pil_at(self._index), *box))
298
+ self._ensure_frame(*box)
299
+ self._draw_overlays(*box)
300
+ if self._first_render:
301
+ self._first_render = False
302
+ if self._autoplay:
303
+ self._arm_autoplay()
304
+
305
+ # ----- navigation --------------------------------------------------------
306
+
307
+ def show(self, index: int, *, direction: int = 0) -> None:
308
+ n = self._count()
309
+ if n == 0 or self._transitioning:
310
+ return
311
+ index = index % n if self._loop else max(0, min(index, n - 1))
312
+ if index == self._index and self._img_id is not None:
313
+ return
314
+ w, h = self._box
315
+ if w <= 1 or h <= 1:
316
+ # Not laid out yet — record the index and announce the change; the
317
+ # first <Configure> will render it.
318
+ self._index = index
319
+ record = self._public_at(index)
320
+ if record is not None:
321
+ self.event_generate("<<Change>>", data=record)
322
+ return
323
+ next_pil = self._compose(self._pil_at(index), w, h)
324
+ self._index = index
325
+
326
+ if self._transition == "none" or self._img_id is None or direction == 0:
327
+ self._set_current(next_pil)
328
+ self._after_change()
329
+ elif self._transition == "fade":
330
+ self._fade_to(next_pil)
331
+ else:
332
+ self._slide_to(next_pil, direction)
333
+
334
+ def next(self) -> None:
335
+ self.show(self._index + 1, direction=1)
336
+
337
+ def previous(self) -> None:
338
+ self.show(self._index - 1, direction=-1)
339
+
340
+ def go_to(self, index: int) -> None:
341
+ direction = (index > self._index) - (index < self._index)
342
+ self.show(index, direction=direction)
343
+
344
+ def _slide_to(self, next_pil: PILImage.Image, d: int) -> None:
345
+ w, h = self._box
346
+ self._transitioning = True
347
+ self._next_photo = PhotoImage(next_pil)
348
+ next_id = self._stage.create_image(d * w, 0, anchor="nw", image=self._next_photo)
349
+ cur_id = self._img_id
350
+ self._stage.tag_lower(next_id)
351
+ if cur_id is not None:
352
+ self._stage.tag_lower(cur_id)
353
+ steps = 8
354
+
355
+ def _ease(t: float) -> float:
356
+ return 2 * t * t if t < 0.5 else 1 - (-2 * t + 2) ** 2 / 2
357
+
358
+ def step(i: int) -> None:
359
+ if i >= steps:
360
+ if cur_id is not None:
361
+ self._stage.delete(cur_id)
362
+ self._stage.coords(next_id, 0, 0)
363
+ self._img_id = next_id
364
+ self._photo = self._next_photo
365
+ self._cur_pil = next_pil
366
+ self._stage.tag_lower(self._img_id)
367
+ self._transitioning = False
368
+ self._after_change()
369
+ return
370
+ t = _ease(i / steps)
371
+ self._stage.coords(next_id, round(d * w * (1 - t)), 0)
372
+ if cur_id is not None:
373
+ self._stage.coords(cur_id, round(-d * w * t), 0)
374
+ self._schedule.delay(_FRAME_MS, lambda: step(i + 1))
375
+
376
+ step(1)
377
+
378
+ def _fade_to(self, next_pil: PILImage.Image) -> None:
379
+ cur = self._cur_pil
380
+ if cur is None:
381
+ self._set_current(next_pil)
382
+ self._after_change()
383
+ return
384
+ self._transitioning = True
385
+ steps = 10
386
+ # Pre-compute all frames up front so PhotoImage allocation doesn't happen
387
+ # inside the after() loop where it blocks the canvas refresh.
388
+ frames = [PhotoImage(PILImage.blend(cur, next_pil, i / steps)) for i in range(1, steps + 1)]
389
+
390
+ def step(i: int) -> None:
391
+ if i >= len(frames):
392
+ self._set_current(next_pil)
393
+ self._transitioning = False
394
+ self._after_change()
395
+ return
396
+ self._fade_photo = frames[i]
397
+ self._stage.itemconfigure(self._img_id, image=self._fade_photo)
398
+ self._schedule.delay(_FRAME_MS + 6, lambda: step(i + 1))
399
+
400
+ step(0)
401
+
402
+ def _after_change(self) -> None:
403
+ bw, bh = self._box
404
+ if bw > 1 and bh > 1:
405
+ # A resize deferred during the transition leaves the settled image at
406
+ # the old size — recompose if it no longer matches the live box.
407
+ if self._cur_pil is None or self._cur_pil.size != (bw, bh):
408
+ self._set_current(self._compose(self._pil_at(self._index), bw, bh))
409
+ self._ensure_frame(bw, bh)
410
+ self._draw_overlays(bw, bh)
411
+ record = self._public_at(self._index)
412
+ if record is not None:
413
+ self.event_generate("<<Change>>", data=record)
414
+ if self._autoplay:
415
+ self._arm_autoplay()
416
+
417
+ # ----- overlays (chevrons, indicator, caption) ---------------------------
418
+
419
+ def _draw_overlays(self, w: int, h: int) -> None:
420
+ self._stage.delete("ov")
421
+ n = self._count()
422
+ self._draw_caption(w, h)
423
+ self._draw_indicator(w, h, n)
424
+ if self._show_arrows and n > 1:
425
+ self._draw_chevrons(w, h)
426
+
427
+ def _nav_photo(self, glyph: str) -> PhotoImage:
428
+ if glyph not in self._nav_photos:
429
+ pad = 3
430
+ canvas = _NAV_SIZE + pad * 2
431
+ base = PILImage.new("RGBA", (canvas, canvas), (0, 0, 0, 0))
432
+ shadow = _ImageService._render_icon(glyph, _NAV_SIZE, "#000000")
433
+ white = _ImageService._render_icon(glyph, _NAV_SIZE, "#ffffff")
434
+ base.alpha_composite(shadow, (pad, pad + 1)) # soft 1px drop shadow
435
+ base.alpha_composite(white, (pad, pad))
436
+ self._nav_photos[glyph] = PhotoImage(base)
437
+ return self._nav_photos[glyph]
438
+
439
+ def _draw_chevrons(self, w: int, h: int) -> None:
440
+ left = self._nav_photo("chevron-left")
441
+ right = self._nav_photo("chevron-right")
442
+ cy = h // 2
443
+ # Tag the chevrons "nav" so the stage's Button-1 handler knows the click
444
+ # was navigation and skips emitting ItemClick (a "break" from an item-tag
445
+ # binding does NOT stop the canvas-wide binding — separate mechanisms).
446
+ lid = self._stage.create_image(_NAV_MARGIN, cy, anchor="w", image=left, tags=("ov", "nav"))
447
+ rid = self._stage.create_image(w - _NAV_MARGIN, cy, anchor="e", image=right, tags=("ov", "nav"))
448
+ self._stage.tag_bind(lid, "<Button-1>", lambda e: self.previous())
449
+ self._stage.tag_bind(rid, "<Button-1>", lambda e: self.next())
450
+
451
+ def _dot_photo(self, key: str, color: str, glyph: str) -> PhotoImage:
452
+ if key not in self._dot_photos:
453
+ self._dot_photos[key] = _ImageService.get_icon(glyph, _DOT_SIZE, color)
454
+ return self._dot_photos[key]
455
+
456
+ def _effective_indicator(self, n: int) -> Indicator:
457
+ if self._indicator == "dots" and n > _DOTS_MAX:
458
+ return "count"
459
+ return self._indicator
460
+
461
+ def _draw_indicator(self, w: int, h: int, n: int) -> None:
462
+ mode = self._effective_indicator(n)
463
+ if mode == "none" or n <= 1:
464
+ return
465
+ y = h - _CAPTION_INSET
466
+ if mode == "count":
467
+ label = f"{self._index + 1} / {n}"
468
+ self._stage.create_text(w // 2 + 1, y + 1, text=label, anchor="s",
469
+ fill="#000000", font=("", 10, "bold"), tags="ov")
470
+ self._stage.create_text(w // 2, y, text=label, anchor="s",
471
+ fill="#ffffff", font=("", 10, "bold"), tags="ov")
472
+ return
473
+ # Neutral by default — solid white active, translucent white inactive;
474
+ # an explicit accent brands the active dot.
475
+ active_color = get_style().style_builder.color(self._accent) if self._accent else "#ffffff"
476
+ active = self._dot_photo(f"active:{active_color}", active_color, "circle-fill")
477
+ inactive = self._dot_photo("inactive", "#ffffff66", "circle-fill")
478
+ total = n * _DOT_SIZE + (n - 1) * _DOT_GAP
479
+ x = w // 2 - total // 2 + _DOT_SIZE // 2
480
+ for i in range(n):
481
+ photo = active if i == self._index else inactive
482
+ did = self._stage.create_image(x, y - _DOT_SIZE // 2, image=photo, tags=("ov", "nav"))
483
+ self._stage.tag_bind(did, "<Button-1>", lambda e, idx=i: self.go_to(idx))
484
+ x += _DOT_SIZE + _DOT_GAP
485
+
486
+ def _draw_caption(self, w: int, h: int) -> None:
487
+ text = self._caption_at(self._index)
488
+ if not text:
489
+ return
490
+ n = self._count()
491
+ bump = _DOT_SIZE + 8 if self._effective_indicator(n) != "none" and n > 1 else 0
492
+ y = h - _CAPTION_INSET - bump
493
+ self._stage.create_text(w // 2 + 1, y + 1, text=text, anchor="s",
494
+ fill="#000000", font=("", 11, "bold"), tags="ov")
495
+ self._stage.create_text(w // 2, y, text=text, anchor="s",
496
+ fill="#ffffff", font=("", 11, "bold"), tags="ov")
497
+
498
+ # ----- autoplay ----------------------------------------------------------
499
+
500
+ @property
501
+ def is_playing(self) -> bool:
502
+ return self._autoplay
503
+
504
+ def play(self) -> None:
505
+ if self._count() <= 1:
506
+ return
507
+ self._autoplay = True
508
+ self._arm_autoplay()
509
+
510
+ def pause(self) -> None:
511
+ self._autoplay = False
512
+ self._cancel_autoplay()
513
+
514
+ def stop(self) -> None:
515
+ """Stop autoplay and return to the first slide."""
516
+ self.pause()
517
+ self.go_to(0)
518
+
519
+ def _arm_autoplay(self) -> None:
520
+ self._cancel_autoplay()
521
+ if self._autoplay:
522
+ self._autoplay_job = self._schedule.delay(self._interval, self._autoplay_tick)
523
+
524
+ def _cancel_autoplay(self) -> None:
525
+ if self._autoplay_job is not None:
526
+ self._autoplay_job.cancel()
527
+ self._autoplay_job = None
528
+
529
+ def _autoplay_tick(self) -> None:
530
+ self._autoplay_job = None
531
+ if self._transitioning:
532
+ # A tick landed mid-transition; next() would no-op and autoplay would
533
+ # die (no _after_change to re-arm). Retry shortly instead.
534
+ if self._autoplay:
535
+ self._autoplay_job = self._schedule.delay(_FRAME_MS * 8, self._autoplay_tick)
536
+ return
537
+ self.next() # _after_change re-arms
538
+
539
+ # ----- events / accessors ------------------------------------------------
540
+
541
+ def _on_stage_click(self, event) -> None:
542
+ self._stage.focus_set()
543
+ # Skip when the click landed on a chevron or dot — that was navigation,
544
+ # not a click on the slide. The canvas-wide binding fires in addition to
545
+ # the item bindings (a "break" from an item binding won't stop it), so
546
+ # check the topmost item at the click point for the "nav" tag.
547
+ hit = self._stage.find_overlapping(event.x, event.y, event.x, event.y)
548
+ if hit and "nav" in self._stage.gettags(hit[-1]):
549
+ return
550
+ record = self._public_at(self._index)
551
+ if record is not None:
552
+ self.event_generate("<<ItemClick>>", data=record)
553
+
554
+ @property
555
+ def index(self) -> int:
556
+ return self._index
557
+
558
+ @property
559
+ def current(self) -> dict | None:
560
+ return self._public_at(self._index)
561
+
562
+ @property
563
+ def count(self) -> int:
564
+ return self._count()
565
+
566
+ def get_datasource(self) -> DataSourceProtocol:
567
+ return self._datasource