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,410 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import overload, Any, Callable, Literal, TYPE_CHECKING
4
+
5
+ from bootstack.widgets._impl.composites.textentry import TextEntry as _InternalTextEntry
6
+ from bootstack.widgets._core.base import PublicWidgetBase, adapt_handler
7
+ from bootstack.widgets._core.events import resolve_event, register_widget_events
8
+ from bootstack.widgets._core.field_mixin import FieldAddonMixin
9
+ from bootstack.events import ChangeEvent, InputEvent, Subscription, ValidationEvent
10
+ from bootstack.streams import Stream
11
+ from bootstack.widgets.types import AccentToken, Event, Justify, WidgetDensity
12
+
13
+ if TYPE_CHECKING:
14
+ import tkinter
15
+
16
+ from bootstack.signals import Signal
17
+
18
+ # Events that fire on the inner entry part, not the outer Frame.
19
+ _INNER_ENTRY_SEQUENCES = frozenset({
20
+ "<<Input>>",
21
+ "<<Change>>",
22
+ "<<Valid>>",
23
+ "<<Invalid>>",
24
+ "<<Validate>>",
25
+ "<Return>",
26
+ "<KeyRelease>",
27
+ "<FocusIn>",
28
+ "<FocusOut>",
29
+ })
30
+
31
+ _TEXTFIELD_EVENTS: dict[str, str] = {
32
+ "input": "<<Input>>",
33
+ "change": "<<Change>>",
34
+ "valid": "<<Valid>>",
35
+ "invalid": "<<Invalid>>",
36
+ "validate": "<<Validate>>",
37
+ "submit": "<Return>",
38
+ "focus": "<FocusIn>",
39
+ "blur": "<FocusOut>",
40
+ }
41
+
42
+
43
+ class TextField(FieldAddonMixin, PublicWidgetBase):
44
+ """Single-line text input with optional label, message, and validation.
45
+
46
+ The initial value is the first positional argument. All options are
47
+ keyword-only.
48
+
49
+ Args:
50
+ value: Initial text value.
51
+ placeholder: Ghost text shown when the field is empty and unfocused.
52
+ textsignal: Reactive `Signal[str]` bound to the field text. The
53
+ field value and signal stay in sync automatically.
54
+ value_format: Format applied when displaying and parsing the value —
55
+ a named preset (e.g. `'decimal'`, `'currency'`, `'percent'`) or a
56
+ custom pattern (e.g. `'#,##0.00'`). Requires localization enabled.
57
+ See :ref:`format specs <value-formats>`.
58
+ label: Label displayed above the input.
59
+ message: Hint or helper text displayed below the input.
60
+ required: If `True`, marks the field as required and prevents empty
61
+ submission.
62
+ mask: Character used to mask each typed character (e.g. `'*'`
63
+ for password inputs).
64
+ read_only: If `True`, text is visible and selectable but not
65
+ editable.
66
+ disabled: If `True`, field is fully non-interactive and dimmed.
67
+ width: Width in character units.
68
+ justify: Text alignment. Default `'left'`.
69
+ font: Semantic font token (e.g. `'body'`, `'code'`). See
70
+ :doc:`/reference/typography`.
71
+ accent: Accent token applied to the focus ring.
72
+ density: Padding density.
73
+ parent: Explicit parent widget. If omitted, the current context-stack
74
+ container is used.
75
+ **kwargs: Layout placement options applied by the parent container —
76
+ `fill`, `expand`, `anchor`, `margin`, `row`, `column`, `sticky`.
77
+ See :doc:`/tasks/layout`.
78
+ """
79
+
80
+ def __init__(
81
+ self,
82
+ value: str = "",
83
+ *,
84
+ placeholder: str | None = None,
85
+ textsignal: "Signal[str] | None" = None,
86
+ value_format: str | None = None,
87
+ label: str | None = None,
88
+ message: str | None = None,
89
+ required: bool = False,
90
+ mask: str | None = None,
91
+ read_only: bool = False,
92
+ disabled: bool = False,
93
+ width: int | None = None,
94
+ justify: Justify | None = None,
95
+ font: str | None = None,
96
+ accent: AccentToken | str | None = None,
97
+ density: WidgetDensity | None = None,
98
+ parent: Any = None,
99
+ **kwargs: Any,
100
+ ) -> None:
101
+ self._parent = self._resolve_parent(parent)
102
+ layout_kw = self._split_layout_kwargs(kwargs)
103
+ tk_master = self._parent._child_master() if self._parent else None
104
+
105
+ internal_kwargs: dict[str, Any] = {}
106
+ if value:
107
+ internal_kwargs["value"] = value
108
+ if placeholder is not None:
109
+ internal_kwargs["placeholder"] = placeholder
110
+ if textsignal is not None:
111
+ internal_kwargs["textsignal"] = textsignal
112
+ if value_format is not None:
113
+ internal_kwargs["value_format"] = value_format
114
+ if label is not None:
115
+ internal_kwargs["label"] = label
116
+ if message is not None:
117
+ internal_kwargs["message"] = message
118
+ if required:
119
+ internal_kwargs["required"] = True
120
+ if mask is not None:
121
+ internal_kwargs["show"] = mask
122
+ if disabled:
123
+ internal_kwargs["state"] = "disabled"
124
+ elif read_only:
125
+ internal_kwargs["state"] = "readonly"
126
+ if width is not None:
127
+ internal_kwargs["width"] = width
128
+ if justify is not None:
129
+ internal_kwargs["justify"] = justify
130
+ if font is not None:
131
+ internal_kwargs["font"] = font
132
+ if accent is not None:
133
+ internal_kwargs["accent"] = accent
134
+ if density is not None:
135
+ internal_kwargs["density"] = density
136
+
137
+ self._internal = _InternalTextEntry(tk_master, **internal_kwargs)
138
+ self._attach_to_parent(layout_kw)
139
+
140
+ # ----- Event routing -----
141
+
142
+ def _entry_widget(self) -> tkinter.Misc:
143
+ return self._internal._entry
144
+
145
+ @overload
146
+ def on(self, event: str) -> Stream: ...
147
+ @overload
148
+ def on(self, event: str, handler: Callable[[Event], Any]) -> Subscription: ...
149
+ def on(self, event: str, handler: Callable[[Event], Any] | None = None) -> Stream | Subscription:
150
+ """Register a callback for an event by name.
151
+
152
+ A generic, string-keyed escape hatch — prefer the typed `on_*`
153
+ shorthands (e.g. `on_change`), which carry the precise payload type.
154
+ Called with no handler, returns a composable `Stream`; with a handler,
155
+ binds it and returns a `Subscription`.
156
+
157
+ Args:
158
+ event: Event name (for example `'change'` or `'focus'`).
159
+ handler: Called with the event payload. Omit to get a composable
160
+ :class:`~bootstack.streams.Stream` instead.
161
+
162
+ Returns:
163
+ A cancellable :class:`~bootstack.events.Subscription` when a handler
164
+ is given, otherwise a :class:`~bootstack.streams.Stream`.
165
+ """
166
+ sequence = resolve_event(self, str(event))
167
+ target = self._entry_widget() if sequence in _INNER_ENTRY_SEQUENCES else self._internal
168
+ if handler is None:
169
+ def _source(h):
170
+ t = self._entry_widget() if sequence in _INNER_ENTRY_SEQUENCES else self._internal
171
+ bid = t.bind(sequence, adapt_handler(h), add="+")
172
+ return Subscription(t, sequence, bid)
173
+ return Stream(self._internal, _source=_source)
174
+ bid = target.bind(sequence, adapt_handler(handler), add="+")
175
+ return Subscription(target, sequence, bid)
176
+
177
+ # ----- Properties -----
178
+
179
+ @property
180
+ def value(self) -> str:
181
+ """The current text value of the field."""
182
+ return self._internal.value
183
+
184
+ @value.setter
185
+ def value(self, v: str) -> None:
186
+ self._internal.value = v
187
+
188
+ @property
189
+ def signal(self) -> "Signal[str] | None":
190
+ """The reactive `Signal` bound to this field, or `None`."""
191
+ return getattr(self._internal, 'signal', None)
192
+
193
+ @property
194
+ def read_only(self) -> bool:
195
+ """Whether the field is visible but not editable."""
196
+ return str(self._entry_widget().cget("state")) == "readonly"
197
+
198
+ @read_only.setter
199
+ def read_only(self, v: bool) -> None:
200
+ self._internal.configure(state="readonly" if v else "normal")
201
+
202
+ @property
203
+ def disabled(self) -> bool:
204
+ """Whether the field is fully non-interactive."""
205
+ return str(self._entry_widget().cget("state")) == "disabled"
206
+
207
+ @disabled.setter
208
+ def disabled(self, v: bool) -> None:
209
+ self._internal.configure(state="disabled" if v else "normal")
210
+
211
+ # ----- Methods -----
212
+
213
+ def validate(self) -> bool:
214
+ """Run validation rules against the current input text.
215
+
216
+ Validates what the user currently sees and is typing — the field's
217
+ display `text`, not its last-committed `value`. This matches the
218
+ automatic blur/key validation, so a manual check and an on-blur check
219
+ agree on the same input.
220
+
221
+ Returns:
222
+ `True` if all rules pass, `False` otherwise.
223
+ """
224
+ entry = self._internal._entry
225
+ return entry.validate(entry._get_validation_value(), trigger="manual")
226
+
227
+ def focus(self) -> None:
228
+ """Give keyboard focus to this field."""
229
+ self._entry_widget().focus_set()
230
+
231
+ def clear(self) -> None:
232
+ """Clear the field text."""
233
+ self._internal.value = ""
234
+
235
+ def select_all(self) -> None:
236
+ """Select all text in the field."""
237
+ self._entry_widget().selection_range(0, "end")
238
+
239
+ def select_range(self, start: int, end: int) -> None:
240
+ """Select text between `start` and `end` character positions.
241
+
242
+ Args:
243
+ start: Start index (0-based, inclusive).
244
+ end: End index (exclusive).
245
+ """
246
+ self._entry_widget().selection_range(start, end)
247
+
248
+ def insert(self, index: int, text: str) -> None:
249
+ """Insert `text` at `index`.
250
+
251
+ Args:
252
+ index: Character position to insert at.
253
+ text: Text to insert.
254
+ """
255
+ self._entry_widget().insert(index, text)
256
+
257
+ def delete(self, start: int, end: int | None = None) -> None:
258
+ """Delete characters from `start` to `end`.
259
+
260
+ Args:
261
+ start: Start index (inclusive).
262
+ end: End index (exclusive). If `None`, deletes to end of field.
263
+ """
264
+ self._entry_widget().delete(start, "end" if end is None else end)
265
+
266
+ # ----- Event shorthands -----
267
+
268
+ @overload
269
+ def on_input(self) -> Stream: ...
270
+ @overload
271
+ def on_input(self, handler: Callable[[InputEvent], Any]) -> Subscription: ...
272
+ def on_input(self, handler: Callable[[InputEvent], Any] | None = None) -> Stream | Subscription:
273
+ """Register a callback fired on every keystroke.
274
+
275
+ Unlike `on_change()` which fires on commit (blur or Enter),
276
+ `on_input()` fires after each character typed. Use it for
277
+ real-time feedback, character counting, or live filtering.
278
+
279
+ Args:
280
+ handler: Called with an :class:`~bootstack.events.InputEvent`. Omit to
281
+ get a composable :class:`~bootstack.streams.Stream` instead.
282
+
283
+ Returns:
284
+ A cancellable :class:`~bootstack.events.Subscription` when a
285
+ handler is given, otherwise a :class:`~bootstack.streams.Stream`.
286
+ """
287
+ return self.on("input", handler)
288
+
289
+ @overload
290
+ def on_change(self) -> Stream: ...
291
+ @overload
292
+ def on_change(self, handler: Callable[[ChangeEvent], Any]) -> Subscription: ...
293
+ def on_change(self, handler: Callable[[ChangeEvent], Any] | None = None) -> Stream | Subscription:
294
+ """Register a callback fired when the field value changes.
295
+
296
+ Args:
297
+ handler: Called with a :class:`~bootstack.events.ChangeEvent` on
298
+ commit (blur or Enter). Omit to get a composable
299
+ :class:`~bootstack.streams.Stream` instead.
300
+
301
+ Returns:
302
+ A cancellable :class:`~bootstack.events.Subscription` when a handler
303
+ is given, otherwise a :class:`~bootstack.streams.Stream`.
304
+ """
305
+ return self.on("change", handler)
306
+
307
+ @overload
308
+ def on_submit(self) -> Stream: ...
309
+ @overload
310
+ def on_submit(self, handler: Callable[[Event], Any]) -> Subscription: ...
311
+ def on_submit(self, handler: Callable[[Event], Any] | None = None) -> Stream | Subscription:
312
+ """Register a callback fired when the user presses Enter.
313
+
314
+ Args:
315
+ handler: Called with an :class:`~bootstack.events.Event`. Omit to
316
+ get a composable :class:`~bootstack.streams.Stream` instead.
317
+
318
+ Returns:
319
+ A cancellable :class:`~bootstack.events.Subscription` when a
320
+ handler is given, otherwise a :class:`~bootstack.streams.Stream`.
321
+ """
322
+ return self.on("submit", handler)
323
+
324
+ @overload
325
+ def on_focus(self) -> Stream: ...
326
+ @overload
327
+ def on_focus(self, handler: Callable[[Event], Any]) -> Subscription: ...
328
+ def on_focus(self, handler: Callable[[Event], Any] | None = None) -> Stream | Subscription:
329
+ """Register a callback fired when the field gains focus.
330
+
331
+ Args:
332
+ handler: Called with an :class:`~bootstack.events.Event`. Omit to
333
+ get a composable :class:`~bootstack.streams.Stream` instead.
334
+
335
+ Returns:
336
+ A cancellable :class:`~bootstack.events.Subscription` when a
337
+ handler is given, otherwise a :class:`~bootstack.streams.Stream`.
338
+ """
339
+ return self.on("focus", handler)
340
+
341
+ @overload
342
+ def on_blur(self) -> Stream: ...
343
+ @overload
344
+ def on_blur(self, handler: Callable[[Event], Any]) -> Subscription: ...
345
+ def on_blur(self, handler: Callable[[Event], Any] | None = None) -> Stream | Subscription:
346
+ """Register a callback fired when the field loses focus.
347
+
348
+ Args:
349
+ handler: Called with an :class:`~bootstack.events.Event`. Omit to
350
+ get a composable :class:`~bootstack.streams.Stream` instead.
351
+
352
+ Returns:
353
+ A cancellable :class:`~bootstack.events.Subscription` when a
354
+ handler is given, otherwise a :class:`~bootstack.streams.Stream`.
355
+ """
356
+ return self.on("blur", handler)
357
+
358
+ @overload
359
+ def on_valid(self) -> Stream: ...
360
+ @overload
361
+ def on_valid(self, handler: Callable[[ValidationEvent], Any]) -> Subscription: ...
362
+ def on_valid(self, handler: Callable[[ValidationEvent], Any] | None = None) -> Stream | Subscription:
363
+ """Register a callback fired when validation passes.
364
+
365
+ Args:
366
+ handler: Called with a :class:`~bootstack.events.ValidationEvent`. Omit to
367
+ get a composable :class:`~bootstack.streams.Stream` instead.
368
+
369
+ Returns:
370
+ A cancellable :class:`~bootstack.events.Subscription` when a
371
+ handler is given, otherwise a :class:`~bootstack.streams.Stream`.
372
+ """
373
+ return self.on("valid", handler)
374
+
375
+ @overload
376
+ def on_invalid(self) -> Stream: ...
377
+ @overload
378
+ def on_invalid(self, handler: Callable[[ValidationEvent], Any]) -> Subscription: ...
379
+ def on_invalid(self, handler: Callable[[ValidationEvent], Any] | None = None) -> Stream | Subscription:
380
+ """Register a callback fired when validation fails.
381
+
382
+ Args:
383
+ handler: Called with a :class:`~bootstack.events.ValidationEvent`. Omit to
384
+ get a composable :class:`~bootstack.streams.Stream` instead.
385
+
386
+ Returns:
387
+ A cancellable :class:`~bootstack.events.Subscription` when a
388
+ handler is given, otherwise a :class:`~bootstack.streams.Stream`.
389
+ """
390
+ return self.on("invalid", handler)
391
+
392
+ @overload
393
+ def on_validate(self) -> Stream: ...
394
+ @overload
395
+ def on_validate(self, handler: Callable[[ValidationEvent], Any]) -> Subscription: ...
396
+ def on_validate(self, handler: Callable[[ValidationEvent], Any] | None = None) -> Stream | Subscription:
397
+ """Register a callback fired after any validation run.
398
+
399
+ Args:
400
+ handler: Called with a :class:`~bootstack.events.ValidationEvent`. Omit to
401
+ get a composable :class:`~bootstack.streams.Stream` instead.
402
+
403
+ Returns:
404
+ A cancellable :class:`~bootstack.events.Subscription` when a
405
+ handler is given, otherwise a :class:`~bootstack.streams.Stream`.
406
+ """
407
+ return self.on("validate", handler)
408
+
409
+
410
+ register_widget_events(TextField, _TEXTFIELD_EVENTS)
@@ -0,0 +1,92 @@
1
+ """ThemeToggle — a sun/moon icon button that flips the application theme."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ from bootstack.style import toggle_theme
7
+ from bootstack.widgets.button import Button
8
+ from bootstack.widgets.types import AccentToken, ButtonVariant, WidgetDensity
9
+
10
+
11
+ def _theme_is_dark() -> bool:
12
+ """Return whether the active theme's mode is dark."""
13
+ from bootstack.style.theme_provider import use_theme
14
+
15
+ try:
16
+ return use_theme().mode == "dark"
17
+ except Exception:
18
+ return False
19
+
20
+
21
+ class ThemeToggle(Button):
22
+ """An icon button that switches between the light and dark theme.
23
+
24
+ Shows the `light_icon` (a sun) in light mode and the `dark_icon` (a moon) in
25
+ dark mode. Clicking it calls `toggle_theme()`; its icon follows the active
26
+ theme, so it stays correct when the theme is changed elsewhere too.
27
+
28
+ It is a plain button — a stateless action whose icon merely reflects the
29
+ current theme — so there is no checked state to keep in sync and no risk of
30
+ looping. Supports the button `variant` and `density`.
31
+
32
+ Args:
33
+ light_icon: Bootstrap Icons name shown in light mode. Default `'sun-fill'`.
34
+ dark_icon: Bootstrap Icons name shown in dark mode. Default
35
+ `'moon-stars-fill'`.
36
+ variant: Button style variant — `'default'`, `'solid'`, `'outline'`, or
37
+ `'ghost'`. Default `'ghost'`.
38
+ density: Padding density — `'default'` or `'compact'`.
39
+ accent: Accent token applied to the icon. Defaults to the foreground.
40
+ parent: Explicit parent widget. If omitted, the current context-stack
41
+ container is used.
42
+ **kwargs: Layout placement options applied by the parent container —
43
+ `fill`, `expand`, `anchor`, `margin`, `row`, `column`, `sticky`.
44
+ See :doc:`/tasks/layout`.
45
+ """
46
+
47
+ def __init__(
48
+ self,
49
+ *,
50
+ light_icon: str = "sun-fill",
51
+ dark_icon: str = "moon-stars-fill",
52
+ variant: ButtonVariant = "ghost",
53
+ density: WidgetDensity = "default",
54
+ accent: AccentToken | str | None = None,
55
+ parent: Any = None,
56
+ **kwargs: Any,
57
+ ) -> None:
58
+ self._light_icon = light_icon
59
+ self._dark_icon = dark_icon
60
+ super().__init__(
61
+ on_click=toggle_theme,
62
+ icon=self._theme_icon(),
63
+ variant=variant,
64
+ density=density,
65
+ accent=accent,
66
+ parent=parent,
67
+ **kwargs,
68
+ )
69
+
70
+ # Keep the icon matching the active theme — clicking this button (or any
71
+ # other theme change) refreshes it. Setting the icon never fires a click,
72
+ # so there is no cycle.
73
+ self._theme_toplevel = self._internal.winfo_toplevel()
74
+ self._theme_bind = self._theme_toplevel.bind(
75
+ "<<BsThemeChanged>>", self._sync_icon, add="+"
76
+ )
77
+ self.on_destroy(self._cleanup_theme_toggle)
78
+
79
+ def _theme_icon(self) -> str:
80
+ return self._dark_icon if _theme_is_dark() else self._light_icon
81
+
82
+ def _sync_icon(self, _event: Any = None) -> None:
83
+ self.icon = self._theme_icon()
84
+
85
+ def _cleanup_theme_toggle(self, _event: Any = None) -> None:
86
+ try:
87
+ self._theme_toplevel.unbind("<<BsThemeChanged>>", self._theme_bind)
88
+ except Exception:
89
+ pass
90
+
91
+
92
+ __all__ = ["ThemeToggle"]