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,851 @@
1
+ """bs.Slider — a themed horizontal or vertical slider widget."""
2
+ from __future__ import annotations
3
+
4
+ import tkinter as tk
5
+ import tkinter.font as tkfont
6
+ from typing import Any, Callable
7
+
8
+ from bootstack.widgets._impl.mixins.configure_mixin import ConfigureDelegationMixin, configure_delegate
9
+ from bootstack.widgets.types import Master, Orient, AccentToken, SurfaceToken, WidgetState
10
+ from bootstack.signals import Signal
11
+ from bootstack.events import SliderEvent, SliderCommitEvent
12
+ from ._shared import (
13
+ HL, HANDLE_SIZE, TRACK_H, HALO_PAD,
14
+ BADGE_H, BADGE_PAD_X, BADGE_GAP, BADGE_FONT_SIZE,
15
+ TICK_GAP, MAJOR_TICK_H, MINOR_TICK_H, LABEL_GAP, LABEL_FONT_SIZE,
16
+ STEP, STEP_LARGE,
17
+ _make_badge, _make_handle, _make_track, _make_focus_ring,
18
+ resolve_colors, get_widget_bg,
19
+ )
20
+
21
+
22
+ class Slider(ConfigureDelegationMixin, tk.Frame):
23
+ # Slider manages its own styling and theming; skip autostyle registration
24
+ # for this widget and all tk children created inside it.
25
+ _bs_autostyle = False
26
+
27
+ """A themed horizontal or vertical slider widget.
28
+
29
+ Replaces `ttk.Scale` with full theming support, optional tick marks,
30
+ value badge, min/max labels, and reactive signal/variable
31
+ binding.
32
+
33
+ The current value is accessible via the `value` property, `get()` /
34
+ `set()` methods, `configure(value=x)`, or a bound `variable` /
35
+ `signal`.
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ master: Master = None,
41
+ *,
42
+ minvalue: float = 0,
43
+ maxvalue: float = 100,
44
+ value: float = 0,
45
+ orient: Orient = "horizontal",
46
+ accent: AccentToken = "primary",
47
+ surface: SurfaceToken = "content",
48
+ variable: tk.DoubleVar | None = None,
49
+ signal: Signal[float] | None = None,
50
+ state: WidgetState = "normal",
51
+ show_value: bool = False,
52
+ show_minmax: bool = False,
53
+ tick_interval: float | None = None,
54
+ minor_ticks: int = 0,
55
+ tick_labels: bool = True,
56
+ tick_format: str = "{:.0f}",
57
+ step: float | None = None,
58
+ **kw: Any,
59
+ ) -> None:
60
+ """Create a Slider widget.
61
+
62
+ Args:
63
+ master: Parent widget.
64
+ minvalue: Minimum value of the slider range.
65
+ maxvalue: Maximum value of the slider range.
66
+ value: Initial value. Ignored when `variable` or `signal` is provided.
67
+ orient: Orientation — `'horizontal'` or `'vertical'`.
68
+ accent: Accent token controlling fill, handle, and focus ring color.
69
+ surface: Surface token for trough and background color context.
70
+ variable: Tkinter DoubleVar to bind to the slider value.
71
+ See [tkinter Variables](https://docs.python.org/3/library/tkinter.html#tkinter-variables).
72
+ signal: Reactive Signal linked to the slider value.
73
+ state: Widget state — `'normal'` or `'disabled'`.
74
+ show_value: Show a floating badge above (horizontal) or beside
75
+ (vertical) the handle displaying the current value.
76
+ show_minmax: Show `minvalue` and `maxvalue` labels at the
77
+ track ends. Redundant when `tick_interval` is set.
78
+ tick_interval: Spacing between major tick marks. `None` disables ticks.
79
+ minor_ticks: Number of minor tick marks between each major tick.
80
+ tick_labels: Show numeric labels at major tick positions.
81
+ tick_format: Format string for tick and badge labels (e.g. `"{:.1f}°C"`).
82
+ step: Snap increment. When set, click/drag values snap to multiples
83
+ of `step` (measured from `minvalue`) and arrow keys move by `step`.
84
+ `None` leaves the value continuous. Independent of `tick_interval`.
85
+ """
86
+ self._minvalue = float(minvalue)
87
+ self._maxvalue = float(maxvalue)
88
+ self._step_size = float(step) if step else None
89
+ self._kbd_step = self._step_size if self._step_size else float(STEP)
90
+ self._kbd_step_large = self._step_size * 10 if self._step_size else float(STEP_LARGE)
91
+ self._orient = orient
92
+ self._accent = accent
93
+ self._surface = surface
94
+ self._state = state
95
+ self._show_value = show_value
96
+ self._show_minmax = show_minmax
97
+ self._tick_interval = tick_interval
98
+ self._minor_ticks = minor_ticks
99
+ self._tick_labels = tick_labels
100
+ self._tick_format = tick_format
101
+
102
+ self._handle_size = HANDLE_SIZE
103
+ self._track_h = TRACK_H
104
+
105
+ # Cross-axis allocation (y for horizontal, x for vertical).
106
+ # For vertical sliders the badge sits to the LEFT of the track, so
107
+ # badge_zone must be wide enough for the widest formatted value.
108
+ if show_value:
109
+ if orient == "vertical":
110
+ _bfont = tkfont.Font(family="TkDefaultFont", size=BADGE_FONT_SIZE, weight="bold")
111
+ _bw = max(_bfont.measure(tick_format.format(float(maxvalue))) + BADGE_PAD_X * 2, BADGE_H)
112
+ self._badge_zone = _bw + BADGE_GAP
113
+ else:
114
+ self._badge_zone = BADGE_H + BADGE_GAP
115
+ else:
116
+ self._badge_zone = 0
117
+ self._tick_zone = self._calc_tick_zone()
118
+ # Reserve HALO_PAD on each side of the handle band so the keyboard focus
119
+ # ring (a halo just outside the handle) is never clipped.
120
+ cross_size = self._badge_zone + HALO_PAD + self._handle_size + HALO_PAD + self._tick_zone
121
+
122
+ # Variable / signal binding — always create a Signal so callers get a
123
+ # clean reactive API; derive the tk.DoubleVar from it.
124
+ if signal is not None:
125
+ self._signal: Signal[float] = signal
126
+ elif variable is not None:
127
+ self._signal = Signal(variable.get())
128
+ # Keep the provided variable in sync via trace rather than replacing it
129
+ self._signal.var.trace_add(
130
+ "write", lambda *_: variable.set(self._signal.var.get()))
131
+ variable.trace_add(
132
+ "write", lambda *_: self._signal.var.set(variable.get()))
133
+ else:
134
+ self._signal = Signal(self._snap(float(value)))
135
+ self._var = self._signal.var
136
+ self._prev_value: float = self._var.get()
137
+
138
+ # Image refs (prevent GC)
139
+ self._handle_photo: Any = None
140
+ self._track_photo: Any = None
141
+
142
+ # Badge geometry (set when show_value=True)
143
+ self._badge_w: int = 0
144
+ self._badge_photo: Any = None
145
+
146
+ # Stable canvas width captured in _on_configure — used for badge clamping
147
+ # to break any circular dependency between badge position and frame size.
148
+ self._cw: int = 0
149
+
150
+ # Tick canvas item IDs
151
+ self._tick_items: list[int] = []
152
+
153
+ kw.setdefault('bd', 0)
154
+ kw.setdefault('relief', 'flat')
155
+
156
+ # Resolve theme colors — use parent's actual background so the slider
157
+ # canvas blends with its container surface and the trough is computed
158
+ # against the real surface color, not always #ffffff.
159
+ self._colors = resolve_colors(self._accent, self._surface, get_widget_bg(master))
160
+
161
+ # Build frame + canvas
162
+ # highlightcolor starts as bg (invisible); shown only on keyboard focus
163
+ if self._orient == "horizontal":
164
+ super().__init__(
165
+ master,
166
+ width=1,
167
+ height=cross_size + HL * 2,
168
+ takefocus=True,
169
+ highlightthickness=HL,
170
+ highlightcolor=self._colors['bg'],
171
+ highlightbackground=self._colors['bg'],
172
+ **kw,
173
+ )
174
+ self.pack_propagate(False)
175
+ self._canvas = tk.Canvas(
176
+ self, width=1, height=cross_size,
177
+ bd=0, highlightthickness=0, bg=self._colors['bg'],
178
+ )
179
+ self._canvas.place(x=0, y=0, relwidth=1, height=cross_size)
180
+ else:
181
+ super().__init__(
182
+ master,
183
+ width=cross_size + HL * 2,
184
+ height=1,
185
+ takefocus=True,
186
+ highlightthickness=HL,
187
+ highlightcolor=self._colors['bg'],
188
+ highlightbackground=self._colors['bg'],
189
+ **kw,
190
+ )
191
+ self.pack_propagate(False)
192
+ self._canvas = tk.Canvas(
193
+ self, width=cross_size, height=1,
194
+ bd=0, highlightthickness=0, bg=self._colors['bg'],
195
+ )
196
+ self._canvas.place(x=0, y=0, relheight=1, width=cross_size)
197
+
198
+ # Center of the track in the cross axis (y for horizontal, x for vertical)
199
+ self._track_center = self._badge_zone + HALO_PAD + self._handle_size // 2
200
+
201
+ # Canvas items — track must be below handle in z-order
202
+ if self._orient == "horizontal":
203
+ self._track_item = self._canvas.create_image(0, self._track_center, anchor="w")
204
+ self._handle_item = self._canvas.create_image(0, self._track_center, anchor="center")
205
+ else:
206
+ self._track_item = self._canvas.create_image(self._track_center, 0, anchor="n")
207
+ self._handle_item = self._canvas.create_image(self._track_center, 0, anchor="center")
208
+
209
+ # Keyboard focus ring — drawn ON the handle (a ring just inside its edge,
210
+ # transparent center), above it in z-order; hidden until the widget takes
211
+ # keyboard focus. Kept within the handle footprint (no overhang) so the
212
+ # track can run edge-to-edge and align with neighboring form fields.
213
+ self._focus_ring_diam = self._handle_size - 2
214
+ self._focus_ring_photo = _make_focus_ring(self._focus_ring_diam, self._colors['focus'])
215
+ self._focus_ring_item = self._canvas.create_image(
216
+ 0, 0, anchor="center", image=self._focus_ring_photo, state="hidden")
217
+
218
+ # Badge items — PIL image background + text item on top
219
+ self._badge_item: int | None = None
220
+ self._badge_text_item: int | None = None
221
+ if self._show_value:
222
+ badge_font = tkfont.Font(family="TkDefaultFont", size=BADGE_FONT_SIZE, weight="bold")
223
+ max_text = self._tick_format.format(self._maxvalue)
224
+ self._badge_w = max(badge_font.measure(max_text) + BADGE_PAD_X * 2, BADGE_H)
225
+ self._badge_photo = _make_badge(self._badge_w, BADGE_H, self._colors['badge_bg'])
226
+ self._badge_item = self._canvas.create_image(0, 0, anchor="nw", image=self._badge_photo)
227
+ self._badge_text_item = self._canvas.create_text(
228
+ 0, 0, text="", fill=self._colors['badge_text'],
229
+ font=badge_font, anchor="center",
230
+ )
231
+
232
+ # Min/max end-labels (only when ticks are not drawn)
233
+ self._minlabel_item: int | None = None
234
+ self._maxlabel_item: int | None = None
235
+ if self._show_minmax and self._tick_interval is None:
236
+ label_font = tkfont.Font(family="TkDefaultFont", size=LABEL_FONT_SIZE)
237
+ if self._orient == "horizontal":
238
+ label_y = self._track_center + self._handle_size // 2 + TICK_GAP + LABEL_GAP
239
+ self._minlabel_item = self._canvas.create_text(
240
+ 0, label_y,
241
+ text=self._tick_format.format(self._minvalue),
242
+ fill=self._colors['tick'], font=label_font, anchor="nw",
243
+ )
244
+ self._maxlabel_item = self._canvas.create_text(
245
+ 0, label_y,
246
+ text=self._tick_format.format(self._maxvalue),
247
+ fill=self._colors['tick'], font=label_font, anchor="ne",
248
+ )
249
+ else:
250
+ label_x = self._track_center + self._handle_size // 2 + TICK_GAP + LABEL_GAP
251
+ self._minlabel_item = self._canvas.create_text(
252
+ label_x, 0,
253
+ text=self._tick_format.format(self._minvalue),
254
+ fill=self._colors['tick'], font=label_font, anchor="w",
255
+ )
256
+ self._maxlabel_item = self._canvas.create_text(
257
+ label_x, 0,
258
+ text=self._tick_format.format(self._maxvalue),
259
+ fill=self._colors['tick'], font=label_font, anchor="w",
260
+ )
261
+
262
+ self._render_handle(self._colors)
263
+
264
+ # Keyboard-vs-mouse focus tracking (ring only on Tab navigation)
265
+ self._mouse_pressed = False
266
+ self._kbd_focused = False # True while the widget holds keyboard focus
267
+
268
+ # Input bindings
269
+ self._canvas.bind("<Button-1>", self._on_click)
270
+ self._canvas.bind("<B1-Motion>", self._on_drag)
271
+ self._canvas.bind("<Button-1>", self._mouse_focus_set, add=True)
272
+ self._canvas.bind("<ButtonRelease-1>", self._on_release)
273
+ self._canvas.tag_bind(self._handle_item, "<Button-1>", self._on_click)
274
+ self._canvas.tag_bind(self._handle_item, "<B1-Motion>", self._on_drag)
275
+ self._canvas.tag_bind(self._handle_item, "<Button-1>", self._mouse_focus_set, add=True)
276
+ self._canvas.tag_bind(self._handle_item, "<ButtonRelease-1>", self._on_release)
277
+
278
+ self.bind("<FocusIn>", self._on_focus_in)
279
+ self.bind("<FocusOut>", self._on_focus_out)
280
+ self.bind("<Left>", lambda e: self._step(-self._kbd_step))
281
+ self.bind("<Right>", lambda e: self._step(+self._kbd_step))
282
+ self.bind("<Down>", lambda e: self._step(-self._kbd_step))
283
+ self.bind("<Up>", lambda e: self._step(+self._kbd_step))
284
+ self.bind("<Shift-Left>", lambda e: self._step(-self._kbd_step_large))
285
+ self.bind("<Shift-Right>", lambda e: self._step(+self._kbd_step_large))
286
+ self.bind("<Home>", lambda e: self._jump(self._minvalue))
287
+ self.bind("<End>", lambda e: self._jump(self._maxvalue))
288
+ self._canvas.bind("<Configure>", self._on_configure)
289
+
290
+ self._var.trace_add("write", self._on_var_write)
291
+ # The unified theme walk re-resolves the track/handle colors via
292
+ # `_bs_apply_theme` on a theme change. The <Map> flush also covers a
293
+ # redraw deferred while the slider was off-screen (value/size changes).
294
+ self.bind("<Map>", lambda e: self._on_map(), add="+")
295
+
296
+ # ------------------------------------------------------------------
297
+ # Layout helpers
298
+ # ------------------------------------------------------------------
299
+
300
+ def _calc_tick_zone(self) -> int:
301
+ if self._tick_interval is not None:
302
+ base = TICK_GAP + MAJOR_TICK_H
303
+ if self._tick_labels:
304
+ if self._orient == "vertical":
305
+ base += LABEL_GAP + self._measure_max_label_width()
306
+ else:
307
+ base += LABEL_GAP + LABEL_FONT_SIZE + 4
308
+ return base
309
+ if self._show_minmax:
310
+ if self._orient == "vertical":
311
+ return TICK_GAP + LABEL_GAP + self._measure_max_label_width()
312
+ return TICK_GAP + LABEL_GAP + LABEL_FONT_SIZE + 4
313
+ return 0
314
+
315
+ def _measure_max_label_width(self) -> int:
316
+ """Return pixel width of the widest tick/minmax label."""
317
+ font = tkfont.Font(family="TkDefaultFont", size=LABEL_FONT_SIZE)
318
+ candidates = [self._tick_format.format(self._minvalue),
319
+ self._tick_format.format(self._maxvalue)]
320
+ return max(font.measure(t) for t in candidates) + 4
321
+
322
+ def _track_range(self) -> tuple[int, int]:
323
+ """Return (start, end) handle travel range in canvas coords (main axis)."""
324
+ # Inset by the handle half-width only, so at min/max the handle's edge sits
325
+ # flush with the canvas edge. The focus ring lives inside the handle now, so
326
+ # no extra overhang has to be reserved.
327
+ edge = self._handle_size // 2
328
+ if self._orient == "horizontal":
329
+ # _cw is the canvas's exact allocated width (from canvas <Configure>).
330
+ w = self._cw if self._cw > 0 else max(1, self._canvas.winfo_width())
331
+ return edge, max(edge + 1, w - edge - 1)
332
+ else:
333
+ h = self.winfo_height() - 2 * HL
334
+ return edge, max(edge + 1, h - edge - 1)
335
+
336
+ def _value_to_pos(self, val: float) -> int:
337
+ """Map a value to a canvas pixel position along the main axis."""
338
+ start, end = self._track_range()
339
+ span = self._maxvalue - self._minvalue
340
+ if span == 0:
341
+ return start
342
+ ratio = max(0.0, min(1.0, (val - self._minvalue) / span))
343
+ if self._orient == "vertical":
344
+ ratio = 1.0 - ratio # vertical: top = max, bottom = min
345
+ return start + int(ratio * (end - start))
346
+
347
+ def _track_extent(self) -> int:
348
+ """Length of the full-width track (handle's min edge to max edge).
349
+
350
+ The track spans `[0, extent]` so it meets the container edges and aligns
351
+ with neighboring form fields; the handle still travels `[r, extent - r]`
352
+ (its center), so ticks/labels — placed at the handle centers — sit `r`
353
+ inside each end and the handle covers that lead-in/out at the extremes.
354
+ """
355
+ _, end = self._track_range()
356
+ return end + self._handle_size // 2
357
+
358
+ def _pos_to_value(self, pos: int) -> float:
359
+ """Map a canvas pixel position to a slider value."""
360
+ start, end = self._track_range()
361
+ if end == start:
362
+ return self._minvalue
363
+ ratio = max(0.0, min(1.0, (pos - start) / (end - start)))
364
+ if self._orient == "vertical":
365
+ ratio = 1.0 - ratio
366
+ return self._minvalue + ratio * (self._maxvalue - self._minvalue)
367
+
368
+ def _snap(self, v: float) -> float:
369
+ """Clamp `v` to [min, max] and, when `step` is set, snap it to the
370
+ nearest grid point (measured from `minvalue`). `maxvalue` stays reachable
371
+ even when the range is not an exact multiple of `step`."""
372
+ v = max(self._minvalue, min(self._maxvalue, float(v)))
373
+ if not self._step_size:
374
+ return v
375
+ grid = self._minvalue + round((v - self._minvalue) / self._step_size) * self._step_size
376
+ grid = max(self._minvalue, min(self._maxvalue, grid))
377
+ # Let the exact maximum win when the value is closer to it than to the
378
+ # last grid point (an off-grid max would otherwise be unreachable).
379
+ if abs(v - self._maxvalue) < abs(v - grid):
380
+ return self._maxvalue
381
+ return grid
382
+
383
+ # ------------------------------------------------------------------
384
+ # Rendering helpers
385
+ # ------------------------------------------------------------------
386
+
387
+ def _render_handle(self, colors: dict[str, str]) -> None:
388
+ fill = colors['handle_disabled'] if self._state == "disabled" else colors['handle']
389
+ self._handle_photo = _make_handle(fill, colors['ring'], colors['handle_border'])
390
+ self._canvas.itemconfig(self._handle_item, image=self._handle_photo)
391
+
392
+ def _update_focus_ring(self) -> None:
393
+ """Position the focus halo over the handle when the widget holds keyboard
394
+ focus; hide it otherwise."""
395
+ item = getattr(self, "_focus_ring_item", None)
396
+ if item is None:
397
+ return
398
+ if not self._kbd_focused or self._state == "disabled":
399
+ self._canvas.itemconfigure(item, state="hidden")
400
+ return
401
+ pos = self._value_to_pos(self._var.get())
402
+ if self._orient == "horizontal":
403
+ self._canvas.coords(item, pos, self._track_center)
404
+ else:
405
+ self._canvas.coords(item, self._track_center, pos)
406
+ self._canvas.itemconfigure(item, state="normal")
407
+ self._canvas.tag_raise(item)
408
+
409
+ def _update_badge_colors(self, colors: dict[str, str]) -> None:
410
+ if self._badge_item is not None:
411
+ self._badge_photo = _make_badge(self._badge_w, BADGE_H, colors['badge_bg'])
412
+ self._canvas.itemconfig(self._badge_item, image=self._badge_photo)
413
+ if self._badge_text_item is not None:
414
+ self._canvas.itemconfig(self._badge_text_item, fill=colors['badge_text'])
415
+
416
+ # ------------------------------------------------------------------
417
+ # Sync: value → visual
418
+ # ------------------------------------------------------------------
419
+
420
+ def _sync(self) -> None:
421
+ val = self._var.get()
422
+ pos = self._value_to_pos(val)
423
+ # Full-width track: spans the handle's edge-to-edge travel (0 .. extent),
424
+ # so it meets the container edges and the handle covers the rounded ends.
425
+ extent = self._track_extent()
426
+
427
+ fill_c = self._colors['fill_disabled'] if self._state == "disabled" else self._colors['fill']
428
+
429
+ if self._orient == "horizontal":
430
+ photo = _make_track(extent, self._track_h, 0, pos,
431
+ self._colors['trough'], fill_c)
432
+ if photo:
433
+ self._track_photo = photo
434
+ self._canvas.coords(self._track_item, 0, self._track_center)
435
+ self._canvas.itemconfig(self._track_item, image=self._track_photo)
436
+ self._canvas.coords(self._handle_item, pos, self._track_center)
437
+ if self._show_value:
438
+ self._update_badge_h(pos, val)
439
+ if self._show_minmax and self._tick_interval is None:
440
+ label_y = self._track_center + self._handle_size // 2 + TICK_GAP + LABEL_GAP
441
+ self._canvas.coords(self._minlabel_item, self._value_to_pos(self._minvalue), label_y)
442
+ self._canvas.coords(self._maxlabel_item, self._value_to_pos(self._maxvalue), label_y)
443
+ else:
444
+ # Vertical: bottom = min, top = max. fill_px = pixels from bottom.
445
+ fill_px = extent - pos
446
+ photo = _make_track(extent, self._track_h, 0, fill_px,
447
+ self._colors['trough'], fill_c,
448
+ vertical=True)
449
+ if photo:
450
+ self._track_photo = photo
451
+ # After rotate(90), image is track_h wide × extent tall; anchor "n"
452
+ self._canvas.coords(self._track_item, self._track_center, 0)
453
+ self._canvas.itemconfig(self._track_item, image=self._track_photo)
454
+ self._canvas.coords(self._handle_item, self._track_center, pos)
455
+ if self._show_value:
456
+ self._update_badge_v(pos, val)
457
+ if self._show_minmax and self._tick_interval is None:
458
+ label_x = self._track_center + self._handle_size // 2 + TICK_GAP + LABEL_GAP
459
+ self._canvas.coords(self._minlabel_item, label_x, self._value_to_pos(self._minvalue))
460
+ self._canvas.coords(self._maxlabel_item, label_x, self._value_to_pos(self._maxvalue))
461
+
462
+ self._update_focus_ring() # keep the halo on the handle
463
+
464
+ def _update_badge_h(self, handle_x: int, val: float) -> None:
465
+ if self._badge_item is None:
466
+ return
467
+ bw = self._badge_w
468
+ # Use stable _cw captured in _on_configure; fall back to live query only if
469
+ # _on_configure has not fired yet (widget not yet laid out).
470
+ cw = self._cw if self._cw > 0 else max(1, self.winfo_width() - 2 * HL)
471
+ # Clamp so image occupies [badge_x, badge_x+bw-1] ⊆ [0, cw-1].
472
+ badge_x = max(0, min(handle_x - bw // 2, cw - bw))
473
+ badge_y = self._badge_zone - BADGE_H - 2
474
+ self._canvas.coords(self._badge_item, badge_x, badge_y)
475
+ if self._badge_text_item is not None:
476
+ self._canvas.coords(self._badge_text_item, badge_x + bw // 2, badge_y + BADGE_H // 2)
477
+ self._canvas.itemconfig(self._badge_text_item, text=self._tick_format.format(val))
478
+
479
+ def _update_badge_v(self, handle_y: int, val: float) -> None:
480
+ if self._badge_item is None:
481
+ return
482
+ bw = self._badge_w
483
+ ch = self.winfo_height() - 2 * HL
484
+ badge_x = self._badge_zone - bw - 2
485
+ badge_y = max(0, min(handle_y - BADGE_H // 2, ch - BADGE_H))
486
+ self._canvas.coords(self._badge_item, badge_x, badge_y)
487
+ if self._badge_text_item is not None:
488
+ self._canvas.coords(self._badge_text_item, badge_x + bw // 2, badge_y + BADGE_H // 2)
489
+ self._canvas.itemconfig(self._badge_text_item, text=self._tick_format.format(val))
490
+
491
+ # ------------------------------------------------------------------
492
+ # Tick marks
493
+ # ------------------------------------------------------------------
494
+
495
+ def _setup_ticks(self) -> None:
496
+ if self._tick_interval is None:
497
+ return
498
+ for item in self._tick_items:
499
+ self._canvas.delete(item)
500
+ self._tick_items.clear()
501
+
502
+ tick_color = self._colors['tick']
503
+ font = tkfont.Font(family="TkDefaultFont", size=LABEL_FONT_SIZE)
504
+ start, end = self._track_range()
505
+ span = self._maxvalue - self._minvalue
506
+ if span <= 0:
507
+ return
508
+
509
+ if self._orient == "horizontal":
510
+ major_y0 = self._track_center + self._handle_size // 2 + TICK_GAP
511
+ major_y1 = major_y0 + MAJOR_TICK_H
512
+ minor_y1 = major_y0 + MINOR_TICK_H
513
+ label_y = major_y1 + LABEL_GAP
514
+
515
+ val = self._minvalue
516
+ while val <= self._maxvalue + 1e-9:
517
+ x = self._value_to_pos(val)
518
+ self._tick_items.append(
519
+ self._canvas.create_line(x, major_y0, x, major_y1, fill=tick_color, width=1))
520
+ if self._tick_labels:
521
+ self._tick_items.append(
522
+ self._canvas.create_text(x, label_y, text=self._tick_format.format(val),
523
+ fill=tick_color, font=font, anchor="n"))
524
+ if self._minor_ticks > 0:
525
+ minor_step = self._tick_interval / (self._minor_ticks + 1)
526
+ for i in range(1, self._minor_ticks + 1):
527
+ mv = val + i * minor_step
528
+ if mv >= self._maxvalue - 1e-9:
529
+ break
530
+ mx = self._value_to_pos(mv)
531
+ self._tick_items.append(
532
+ self._canvas.create_line(mx, major_y0, mx, minor_y1,
533
+ fill=tick_color, width=1))
534
+ val += self._tick_interval
535
+ else:
536
+ major_x0 = self._track_center + self._handle_size // 2 + TICK_GAP
537
+ major_x1 = major_x0 + MAJOR_TICK_H
538
+ minor_x1 = major_x0 + MINOR_TICK_H
539
+ label_x = major_x1 + LABEL_GAP
540
+
541
+ val = self._minvalue
542
+ while val <= self._maxvalue + 1e-9:
543
+ y = self._value_to_pos(val)
544
+ self._tick_items.append(
545
+ self._canvas.create_line(major_x0, y, major_x1, y, fill=tick_color, width=1))
546
+ if self._tick_labels:
547
+ self._tick_items.append(
548
+ self._canvas.create_text(label_x, y, text=self._tick_format.format(val),
549
+ fill=tick_color, font=font, anchor="w"))
550
+ if self._minor_ticks > 0:
551
+ minor_step = self._tick_interval / (self._minor_ticks + 1)
552
+ for i in range(1, self._minor_ticks + 1):
553
+ mv = val + i * minor_step
554
+ if mv >= self._maxvalue - 1e-9:
555
+ break
556
+ my = self._value_to_pos(mv)
557
+ self._tick_items.append(
558
+ self._canvas.create_line(major_x0, my, minor_x1, my,
559
+ fill=tick_color, width=1))
560
+ val += self._tick_interval
561
+
562
+ # ------------------------------------------------------------------
563
+ # Event handlers
564
+ # ------------------------------------------------------------------
565
+
566
+ def _on_configure(self, event: tk.Event) -> None:
567
+ # Bound to canvas <Configure>, so event.width/height IS the canvas size —
568
+ # no border arithmetic needed.
569
+ if self._orient == "horizontal" and event.width > self._handle_size:
570
+ self._cw = event.width
571
+ self._canvas.configure(scrollregion=(0, 0, event.width, event.height))
572
+ self._setup_ticks()
573
+ self._sync()
574
+ elif self._orient == "vertical" and event.height > self._handle_size:
575
+ self._canvas.configure(scrollregion=(0, 0, event.width, event.height))
576
+ self._setup_ticks()
577
+ self._sync()
578
+
579
+ def _on_var_write(self, *_: Any) -> None:
580
+ self._sync()
581
+ val = self._var.get()
582
+ self.event_generate("<<Change>>", data=SliderEvent(value=val, prev_value=self._prev_value))
583
+ self._prev_value = val
584
+
585
+ def _mouse_focus_set(self, _event: tk.Event) -> None:
586
+ self._mouse_pressed = True
587
+ self.focus_set()
588
+
589
+ def _on_focus_in(self, _event: tk.Event) -> None:
590
+ if self._mouse_pressed:
591
+ self._mouse_pressed = False
592
+ return
593
+ self._kbd_focused = True
594
+ # The handle halo is the focus indicator — no whole-widget frame ring.
595
+ self._update_focus_ring()
596
+
597
+ def _on_focus_out(self, _event: tk.Event) -> None:
598
+ self._kbd_focused = False
599
+ self._update_focus_ring()
600
+
601
+ def _on_click(self, event: tk.Event) -> None:
602
+ if self._state == "disabled":
603
+ return
604
+ # event.x/y are already in canvas coordinates — no winfo_x/y offset needed.
605
+ if self._orient == "horizontal":
606
+ self._var.set(self._snap(self._pos_to_value(event.x)))
607
+ else:
608
+ self._var.set(self._snap(self._pos_to_value(event.y)))
609
+
610
+ def _on_drag(self, event: tk.Event) -> None:
611
+ self._on_click(event)
612
+
613
+ def _on_release(self, event: tk.Event) -> None:
614
+ if self._state == "disabled":
615
+ return
616
+ val = self._var.get()
617
+ self.event_generate("<<Commit>>", data=SliderCommitEvent(value=val))
618
+
619
+ def _step(self, amount: float) -> None:
620
+ if self._state == "disabled":
621
+ return
622
+ new = self._snap(self._var.get() + amount)
623
+ self._var.set(new)
624
+ self.event_generate("<<Commit>>", data=SliderCommitEvent(value=new))
625
+
626
+ def _jump(self, value: float) -> None:
627
+ """Keyboard jump (Home/End) — set then commit, like _step does."""
628
+ if self._state == "disabled":
629
+ return
630
+ new = max(self._minvalue, min(self._maxvalue, float(value)))
631
+ self._var.set(new)
632
+ self.event_generate("<<Commit>>", data=SliderCommitEvent(value=new))
633
+
634
+ def _reclamp(self) -> None:
635
+ """Pull the current value back into [min, max] (and onto the step grid)
636
+ after a range or step change."""
637
+ cur = self._var.get()
638
+ clamped = self._snap(cur)
639
+ if clamped != cur:
640
+ self._var.set(clamped) # trace fires _sync + <<Change>>
641
+ else:
642
+ self._sync()
643
+
644
+ def _on_map(self) -> None:
645
+ if getattr(self, '_theme_update_pending', False):
646
+ self._on_theme_changed()
647
+
648
+ def _bs_apply_theme(self) -> None:
649
+ """Theme walk hook — re-resolve colors and redraw track, handle, badge,
650
+ focus halo, and tick marks + tick labels. The walk owns visibility
651
+ gating, so this always repaints (no early-out).
652
+ """
653
+ self._theme_update_pending = False
654
+ self._colors = resolve_colors(self._accent, self._surface, get_widget_bg(self.master))
655
+ colors = self._colors
656
+ self.configure(
657
+ highlightbackground=colors['bg'],
658
+ highlightcolor=colors['bg'], # reset to hidden; re-shown on next keyboard focus
659
+ )
660
+ self._canvas.configure(bg=colors['bg'])
661
+ self._render_handle(colors)
662
+ self._update_badge_colors(colors)
663
+ # Recolor the focus halo to the new theme's focus color.
664
+ self._focus_ring_photo = _make_focus_ring(self._focus_ring_diam, colors['focus'])
665
+ self._canvas.itemconfig(self._focus_ring_item, image=self._focus_ring_photo)
666
+ if self._minlabel_item is not None:
667
+ self._canvas.itemconfig(self._minlabel_item, fill=colors['tick'])
668
+ if self._maxlabel_item is not None:
669
+ self._canvas.itemconfig(self._maxlabel_item, fill=colors['tick'])
670
+ self._setup_ticks() # redraws ticks + tick labels with colors['tick']
671
+ self._sync() # _sync repositions/shows the halo as needed
672
+
673
+ def _on_theme_changed(self) -> None:
674
+ # Value/size redraws keep their own off-screen deferral (flushed on
675
+ # <Map>); a genuine theme change is driven through _bs_apply_theme by the
676
+ # unified theme walk.
677
+ if not self.winfo_viewable():
678
+ self._theme_update_pending = True
679
+ return
680
+ self._bs_apply_theme()
681
+
682
+ # ------------------------------------------------------------------
683
+ # Public API
684
+ # ------------------------------------------------------------------
685
+
686
+ @property
687
+ def signal(self) -> Signal[float]:
688
+ """Reactive Signal linked to the slider value."""
689
+ return self._signal
690
+
691
+ @property
692
+ def value(self) -> float:
693
+ """Current slider value."""
694
+ return self._var.get()
695
+
696
+ def get(self) -> float:
697
+ """Return the current slider value."""
698
+ return self._var.get()
699
+
700
+ def set(self, value: float) -> None:
701
+ """Set the slider to a new value, clamped to [minvalue, maxvalue] and
702
+ snapped to the step grid when `step` is set.
703
+
704
+ Args:
705
+ value: New value to set.
706
+ """
707
+ self._var.set(self._snap(float(value)))
708
+
709
+ def on_changed(self, callback: Callable[[SliderEvent], None]) -> str:
710
+ """Register a callback for `<<Change>>` events.
711
+
712
+ Args:
713
+ callback: Called with a Tkinter event; `event.value` is
714
+ the current value and `event.prev_value` is the
715
+ value before the change.
716
+
717
+ Returns:
718
+ Bind ID — pass to `off_changed()` to unsubscribe.
719
+ """
720
+ return self.bind("<<Change>>", callback, add="+")
721
+
722
+ def off_changed(self, bind_id: str | None = None) -> None:
723
+ """Unsubscribe from `<<Change>>`.
724
+
725
+ Args:
726
+ bind_id: ID returned by `on_changed()`. If None, removes all.
727
+ """
728
+ self.unbind("<<Change>>", bind_id)
729
+
730
+ def on_commit(self, callback: Callable[[SliderCommitEvent], None]) -> str:
731
+ """Register a callback for `<<Commit>>` events.
732
+
733
+ Fires on mouse release and keyboard commit, not continuously during drag.
734
+ Use for expensive downstream operations.
735
+
736
+ Args:
737
+ callback: Called with a Tkinter event; `event.value` is
738
+ the committed value.
739
+
740
+ Returns:
741
+ Bind ID — pass to `off_commit()` to unsubscribe.
742
+ """
743
+ return self.bind("<<Commit>>", callback, add="+")
744
+
745
+ def off_commit(self, bind_id: str | None = None) -> None:
746
+ """Unsubscribe from `<<Commit>>`.
747
+
748
+ Args:
749
+ bind_id: ID returned by `on_commit()`. If None, removes all.
750
+ """
751
+ self.unbind("<<Commit>>", bind_id)
752
+
753
+ @property
754
+ def variable(self) -> tk.DoubleVar:
755
+ """Underlying Tkinter DoubleVar.
756
+
757
+ See [tkinter Variables](https://docs.python.org/3/library/tkinter.html#tkinter-variables).
758
+ """
759
+ return self._var
760
+
761
+ # ------------------------------------------------------------------
762
+ # Configure delegates
763
+ # ------------------------------------------------------------------
764
+
765
+ @configure_delegate('value')
766
+ def _delegate_value(self, value=None):
767
+ if value is None:
768
+ return self._var.get()
769
+ self.set(value)
770
+
771
+ @configure_delegate('minvalue')
772
+ def _delegate_minvalue(self, value=None):
773
+ if value is None:
774
+ return self._minvalue
775
+ self._minvalue = float(value)
776
+ self._setup_ticks()
777
+ self._reclamp()
778
+
779
+ @configure_delegate('maxvalue')
780
+ def _delegate_maxvalue(self, value=None):
781
+ if value is None:
782
+ return self._maxvalue
783
+ self._maxvalue = float(value)
784
+ self._setup_ticks()
785
+ self._reclamp()
786
+
787
+ @configure_delegate('step')
788
+ def _delegate_step(self, value=None):
789
+ if value is None:
790
+ return self._step_size
791
+ self._step_size = float(value) if value else None
792
+ self._kbd_step = self._step_size if self._step_size else float(STEP)
793
+ self._kbd_step_large = self._step_size * 10 if self._step_size else float(STEP_LARGE)
794
+ self._reclamp() # re-snap the current value onto the new grid
795
+
796
+ @configure_delegate('accent')
797
+ def _delegate_accent(self, value=None):
798
+ if value is None:
799
+ return self._accent
800
+ self._accent = value
801
+ self._on_theme_changed()
802
+
803
+ @configure_delegate('state')
804
+ def _delegate_state(self, value=None):
805
+ if value is None:
806
+ return self._state
807
+ self._state = value
808
+ self._render_handle(self._colors)
809
+ self._sync()
810
+
811
+ @configure_delegate('tick_format')
812
+ def _delegate_tick_format(self, value=None):
813
+ if value is None:
814
+ return self._tick_format
815
+ self._tick_format = value
816
+ self._setup_ticks()
817
+ self._sync()
818
+
819
+ @configure_delegate('tick_interval')
820
+ def _delegate_tick_interval(self, value=None):
821
+ if value is None:
822
+ return self._tick_interval
823
+ self._tick_interval = value
824
+ self._setup_ticks()
825
+
826
+ @configure_delegate('show_value')
827
+ def _delegate_show_value(self, value=None):
828
+ if value is None:
829
+ return self._show_value
830
+ # show_value creates/destroys canvas items; must be set at construction time.
831
+
832
+ @configure_delegate('minor_ticks')
833
+ def _delegate_minor_ticks(self, value=None):
834
+ if value is None:
835
+ return self._minor_ticks
836
+ self._minor_ticks = int(value)
837
+ self._setup_ticks()
838
+
839
+ @configure_delegate('tick_labels')
840
+ def _delegate_tick_labels(self, value=None):
841
+ if value is None:
842
+ return self._tick_labels
843
+ self._tick_labels = bool(value)
844
+ self._setup_ticks()
845
+
846
+ @configure_delegate('surface')
847
+ def _delegate_surface(self, value=None):
848
+ if value is None:
849
+ return self._surface
850
+ self._surface = value
851
+ self._on_theme_changed()