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,982 @@
1
+ """bs.RangeSlider — a themed two-handle range 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 RangeSliderEvent, RangeSliderCommitEvent
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 RangeSlider(ConfigureDelegationMixin, tk.Frame):
23
+ _bs_autostyle = False
24
+
25
+ """A themed two-handle range slider widget.
26
+
27
+ Provides a low-value and high-value handle that define a selected range
28
+ within `[minvalue, maxvalue]`. Keyboard model: the slider is a single tab
29
+ stop (`Tab` moves focus in, then straight out — never trapped). The
30
+ primary-axis arrows move the active handle; the cross-axis arrows switch
31
+ which handle is active, and the active handle shows a focus ring.
32
+
33
+ Current range is accessible via `lovalue` / `hivalue` properties,
34
+ `get_lo()` / `get_hi()` / `set_lo()` / `set_hi()` methods, or
35
+ bound `lo_variable` / `hi_variable` / `lo_signal` / `hi_signal`.
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ master: Master = None,
41
+ *,
42
+ minvalue: float = 0,
43
+ maxvalue: float = 100,
44
+ lovalue: float = 0,
45
+ hivalue: float = 100,
46
+ orient: Orient = "horizontal",
47
+ accent: AccentToken = "primary",
48
+ surface: SurfaceToken = "content",
49
+ lo_variable: tk.DoubleVar | None = None,
50
+ hi_variable: tk.DoubleVar | None = None,
51
+ lo_signal: Signal[float] | None = None,
52
+ hi_signal: Signal[float] | None = None,
53
+ state: WidgetState = "normal",
54
+ show_value: bool = False,
55
+ tick_interval: float | None = None,
56
+ minor_ticks: int = 0,
57
+ tick_labels: bool = True,
58
+ tick_format: str = "{:.0f}",
59
+ step: float | None = None,
60
+ **kw: Any,
61
+ ) -> None:
62
+ """Create a RangeSlider widget.
63
+
64
+ Args:
65
+ master: Parent widget.
66
+ minvalue: Minimum bound of the slider range.
67
+ maxvalue: Maximum bound of the slider range.
68
+ lovalue: Initial low-handle value. Ignored when `lo_variable`
69
+ or `lo_signal` is provided.
70
+ hivalue: Initial high-handle value. Ignored when `hi_variable`
71
+ or `hi_signal` is provided.
72
+ orient: Orientation — `'horizontal'` or `'vertical'`.
73
+ accent: Accent token controlling fill, handles, and focus ring color.
74
+ surface: Surface token for trough and background color context.
75
+ lo_variable: Tkinter DoubleVar bound to the low-handle value.
76
+ See [tkinter Variables](https://docs.python.org/3/library/tkinter.html#tkinter-variables).
77
+ hi_variable: Tkinter DoubleVar bound to the high-handle value.
78
+ See [tkinter Variables](https://docs.python.org/3/library/tkinter.html#tkinter-variables).
79
+ lo_signal: Reactive Signal linked to the low-handle value.
80
+ hi_signal: Reactive Signal linked to the high-handle value.
81
+ state: Widget state — `'normal'` or `'disabled'`.
82
+ show_value: Show floating badges above/beside each handle displaying
83
+ their current values.
84
+ tick_interval: Spacing between major tick marks. `None` disables ticks.
85
+ minor_ticks: Number of minor tick marks between each major tick.
86
+ tick_labels: Show numeric labels at major tick positions.
87
+ tick_format: Format string for tick and badge labels (e.g. `"{:.1f}°C"`).
88
+ step: Snap increment. When set, handle values snap to multiples of
89
+ `step` (measured from `minvalue`) and arrow keys move by `step`.
90
+ `None` leaves the values continuous. Independent of `tick_interval`.
91
+ """
92
+ self._minvalue = float(minvalue)
93
+ self._maxvalue = float(maxvalue)
94
+ self._step_size = float(step) if step else None
95
+ self._kbd_step = self._step_size if self._step_size else float(STEP)
96
+ self._kbd_step_large = self._step_size * 10 if self._step_size else float(STEP_LARGE)
97
+ self._orient = orient
98
+ self._accent = accent
99
+ self._surface = surface
100
+ self._state = state
101
+ self._show_value = show_value
102
+ self._tick_interval = tick_interval
103
+ self._minor_ticks = minor_ticks
104
+ self._tick_labels = tick_labels
105
+ self._tick_format = tick_format
106
+
107
+ self._handle_size = HANDLE_SIZE
108
+ self._track_h = TRACK_H
109
+
110
+ if show_value:
111
+ if orient == "vertical":
112
+ _bfont = tkfont.Font(family="TkDefaultFont", size=BADGE_FONT_SIZE, weight="bold")
113
+ _bw = max(_bfont.measure(tick_format.format(float(maxvalue))) + BADGE_PAD_X * 2, BADGE_H)
114
+ self._badge_zone = _bw + BADGE_GAP
115
+ else:
116
+ self._badge_zone = BADGE_H + BADGE_GAP
117
+ else:
118
+ self._badge_zone = 0
119
+ self._tick_zone = self._calc_tick_zone()
120
+ # Reserve HALO_PAD on each side of the handle band so the keyboard focus
121
+ # ring (a halo just outside the active handle) is never clipped.
122
+ cross_size = self._badge_zone + HALO_PAD + self._handle_size + HALO_PAD + self._tick_zone
123
+
124
+ # Variable / signal binding for lo and hi — always create a Signal so
125
+ # callers get a clean reactive API; derive the tk.DoubleVar from it.
126
+ if lo_signal is not None:
127
+ self._lo_signal: Signal[float] = lo_signal
128
+ elif lo_variable is not None:
129
+ self._lo_signal = Signal(lo_variable.get())
130
+ self._lo_signal.var.trace_add(
131
+ "write", lambda *_: lo_variable.set(self._lo_signal.var.get()))
132
+ lo_variable.trace_add(
133
+ "write", lambda *_: self._lo_signal.var.set(lo_variable.get()))
134
+ else:
135
+ self._lo_signal = Signal(self._snap(float(lovalue)))
136
+ self._lo_var = self._lo_signal.var
137
+
138
+ if hi_signal is not None:
139
+ self._hi_signal: Signal[float] = hi_signal
140
+ elif hi_variable is not None:
141
+ self._hi_signal = Signal(hi_variable.get())
142
+ self._hi_signal.var.trace_add(
143
+ "write", lambda *_: hi_variable.set(self._hi_signal.var.get()))
144
+ hi_variable.trace_add(
145
+ "write", lambda *_: self._hi_signal.var.set(hi_variable.get()))
146
+ else:
147
+ self._hi_signal = Signal(self._snap(float(hivalue)))
148
+ self._hi_var = self._hi_signal.var
149
+
150
+ self._prev_lovalue: float = self._lo_var.get()
151
+ self._prev_hivalue: float = self._hi_var.get()
152
+
153
+ # Image refs (prevent GC)
154
+ self._handle_lo_photo: Any = None
155
+ self._handle_hi_photo: Any = None
156
+ self._track_photo: Any = None
157
+
158
+ # Badge geometry (set when show_value=True)
159
+ self._badge_w: int = 0
160
+ self._badge_lo_photo: Any = None
161
+ self._badge_hi_photo: Any = None
162
+
163
+ # Stable canvas width captured in _on_configure — used for badge clamping.
164
+ self._cw: int = 0
165
+
166
+ self._tick_items: list[int] = []
167
+
168
+ # Drag / keyboard focus state
169
+ self._dragging: str | None = None # "lo" | "hi" | None
170
+ self._focus_handle = "lo"
171
+ self._kbd_focused = False # True while the widget holds keyboard focus
172
+ # (gates the active-handle focus ring; set
173
+ # before the first _render_handles below)
174
+
175
+ kw.setdefault('bd', 0)
176
+ kw.setdefault('relief', 'flat')
177
+
178
+ self._colors = resolve_colors(self._accent, self._surface, get_widget_bg(master))
179
+
180
+ if self._orient == "horizontal":
181
+ super().__init__(
182
+ master,
183
+ width=1,
184
+ height=cross_size + HL * 2,
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=1, height=cross_size,
194
+ bd=0, highlightthickness=0, bg=self._colors['bg'],
195
+ )
196
+ self._canvas.place(x=0, y=0, relwidth=1, height=cross_size)
197
+ else:
198
+ super().__init__(
199
+ master,
200
+ width=cross_size + HL * 2,
201
+ height=1,
202
+ takefocus=True,
203
+ highlightthickness=HL,
204
+ highlightcolor=self._colors['bg'],
205
+ highlightbackground=self._colors['bg'],
206
+ **kw,
207
+ )
208
+ self.pack_propagate(False)
209
+ self._canvas = tk.Canvas(
210
+ self, width=cross_size, height=1,
211
+ bd=0, highlightthickness=0, bg=self._colors['bg'],
212
+ )
213
+ self._canvas.place(x=0, y=0, relheight=1, width=cross_size)
214
+
215
+ self._track_center = self._badge_zone + HALO_PAD + self._handle_size // 2
216
+
217
+ if self._orient == "horizontal":
218
+ self._track_item = self._canvas.create_image(0, self._track_center, anchor="w")
219
+ self._handle_lo_item = self._canvas.create_image(0, self._track_center, anchor="center")
220
+ self._handle_hi_item = self._canvas.create_image(0, self._track_center, anchor="center")
221
+ else:
222
+ self._track_item = self._canvas.create_image(self._track_center, 0, anchor="n")
223
+ self._handle_lo_item = self._canvas.create_image(self._track_center, 0, anchor="center")
224
+ self._handle_hi_item = self._canvas.create_image(self._track_center, 0, anchor="center")
225
+
226
+ # Keyboard focus ring — drawn ON the active handle (a ring just inside its
227
+ # edge, transparent center), above both handles. Within the handle
228
+ # footprint (no overhang) so the track runs edge-to-edge. Hidden until the
229
+ # widget takes keyboard focus.
230
+ self._focus_ring_diam = self._handle_size - 2
231
+ self._focus_ring_photo = _make_focus_ring(self._focus_ring_diam, self._colors['focus'])
232
+ self._focus_ring_item = self._canvas.create_image(
233
+ 0, 0, anchor="center", image=self._focus_ring_photo, state="hidden")
234
+
235
+ # Badge items — PIL image background + text items on top
236
+ self._badge_lo_item: int | None = None
237
+ self._badge_hi_item: int | None = None
238
+ self._badge_lo_text: int | None = None
239
+ self._badge_hi_text: int | None = None
240
+ if self._show_value:
241
+ badge_font = tkfont.Font(family="TkDefaultFont", size=BADGE_FONT_SIZE, weight="bold")
242
+ max_text = self._tick_format.format(self._maxvalue)
243
+ self._badge_w = max(badge_font.measure(max_text) + BADGE_PAD_X * 2, BADGE_H)
244
+ _bg = self._colors['badge_bg']
245
+ self._badge_lo_photo = _make_badge(self._badge_w, BADGE_H, _bg)
246
+ self._badge_hi_photo = _make_badge(self._badge_w, BADGE_H, _bg)
247
+ self._badge_lo_item = self._canvas.create_image(0, 0, anchor="nw", image=self._badge_lo_photo)
248
+ self._badge_hi_item = self._canvas.create_image(0, 0, anchor="nw", image=self._badge_hi_photo)
249
+ self._badge_lo_text = self._canvas.create_text(
250
+ 0, 0, text="", fill=self._colors['badge_text'],
251
+ font=badge_font, anchor="center",
252
+ )
253
+ self._badge_hi_text = self._canvas.create_text(
254
+ 0, 0, text="", fill=self._colors['badge_text'],
255
+ font=badge_font, anchor="center",
256
+ )
257
+
258
+ self._render_handles(self._colors)
259
+
260
+ # Keyboard-vs-mouse focus tracking (ring only on Tab navigation)
261
+ self._mouse_pressed = False
262
+
263
+ # Canvas-level click: pick nearest handle
264
+ self._canvas.bind("<Button-1>", self._on_canvas_click)
265
+ self._canvas.bind("<B1-Motion>", self._on_canvas_drag)
266
+ self._canvas.bind("<Button-1>", self._mouse_focus_set, add=True)
267
+ self._canvas.bind("<ButtonRelease-1>", self._on_release)
268
+
269
+ # Per-handle drag bindings
270
+ self._canvas.tag_bind(self._handle_lo_item, "<Button-1>",
271
+ lambda e: self._handle_press(e, "lo"))
272
+ self._canvas.tag_bind(self._handle_lo_item, "<B1-Motion>", self._on_handle_drag)
273
+ self._canvas.tag_bind(self._handle_lo_item, "<ButtonRelease-1>", self._on_release)
274
+ self._canvas.tag_bind(self._handle_hi_item, "<Button-1>",
275
+ lambda e: self._handle_press(e, "hi"))
276
+ self._canvas.tag_bind(self._handle_hi_item, "<B1-Motion>", self._on_handle_drag)
277
+ self._canvas.tag_bind(self._handle_hi_item, "<ButtonRelease-1>", self._on_release)
278
+
279
+ self.bind("<FocusIn>", self._on_focus_in)
280
+ self.bind("<FocusOut>", self._on_focus_out)
281
+ # Keyboard model: the slider is a SINGLE tab stop. We bind NO <Tab>, so
282
+ # Tk's normal focus traversal moves focus in and straight back out — focus
283
+ # is never trapped. The primary-axis arrows move the active handle; the
284
+ # cross-axis arrows switch which handle is active (a two-handle slider needs
285
+ # a switch gesture, and Tab is reserved for leaving the widget).
286
+ self.bind("<Home>", lambda e: self._jump(self._minvalue))
287
+ self.bind("<End>", lambda e: self._jump(self._maxvalue))
288
+ if self._orient == "horizontal":
289
+ self.bind("<Left>", lambda e: self._step(-self._kbd_step))
290
+ self.bind("<Right>", lambda e: self._step(+self._kbd_step))
291
+ self.bind("<Shift-Left>", lambda e: self._step(-self._kbd_step_large))
292
+ self.bind("<Shift-Right>", lambda e: self._step(+self._kbd_step_large))
293
+ self.bind("<Down>", lambda e: self._select_handle("lo"))
294
+ self.bind("<Up>", lambda e: self._select_handle("hi"))
295
+ else:
296
+ self.bind("<Down>", lambda e: self._step(-self._kbd_step))
297
+ self.bind("<Up>", lambda e: self._step(+self._kbd_step))
298
+ self.bind("<Shift-Down>", lambda e: self._step(-self._kbd_step_large))
299
+ self.bind("<Shift-Up>", lambda e: self._step(+self._kbd_step_large))
300
+ self.bind("<Left>", lambda e: self._select_handle("lo"))
301
+ self.bind("<Right>", lambda e: self._select_handle("hi"))
302
+ self._canvas.bind("<Configure>", self._on_configure)
303
+
304
+ self._lo_var.trace_add("write", self._on_lo_write)
305
+ self._hi_var.trace_add("write", self._on_hi_write)
306
+ # The unified theme walk re-resolves track/handle colors via
307
+ # `_bs_apply_theme` on a theme change. The <Map> flush also covers a
308
+ # redraw deferred while the slider was off-screen (value/size changes).
309
+ self.bind("<Map>", lambda e: self._on_map(), add="+")
310
+
311
+ # ------------------------------------------------------------------
312
+ # Layout helpers
313
+ # ------------------------------------------------------------------
314
+
315
+ def _calc_tick_zone(self) -> int:
316
+ if self._tick_interval is None:
317
+ return 0
318
+ base = TICK_GAP + MAJOR_TICK_H
319
+ if self._tick_labels:
320
+ if self._orient == "vertical":
321
+ base += LABEL_GAP + self._measure_max_label_width()
322
+ else:
323
+ base += LABEL_GAP + LABEL_FONT_SIZE + 4
324
+ return base
325
+
326
+ def _measure_max_label_width(self) -> int:
327
+ font = tkfont.Font(family="TkDefaultFont", size=LABEL_FONT_SIZE)
328
+ candidates = [self._tick_format.format(self._minvalue),
329
+ self._tick_format.format(self._maxvalue)]
330
+ return max(font.measure(t) for t in candidates) + 4
331
+
332
+ def _track_range(self) -> tuple[int, int]:
333
+ # Inset by the handle half-width only — the handle edge sits flush with the
334
+ # canvas edge at the extremes, and the in-handle focus ring needs no overhang.
335
+ edge = self._handle_size // 2
336
+ if self._orient == "horizontal":
337
+ w = self._cw if self._cw > 0 else max(1, self._canvas.winfo_width())
338
+ return edge, max(edge + 1, w - edge - 1)
339
+ else:
340
+ h = self.winfo_height() - 2 * HL
341
+ return edge, max(edge + 1, h - edge - 1)
342
+
343
+ def _value_to_pos(self, val: float) -> int:
344
+ start, end = self._track_range()
345
+ span = self._maxvalue - self._minvalue
346
+ if span == 0:
347
+ return start
348
+ ratio = max(0.0, min(1.0, (val - self._minvalue) / span))
349
+ if self._orient == "vertical":
350
+ ratio = 1.0 - ratio
351
+ return start + int(ratio * (end - start))
352
+
353
+ def _track_extent(self) -> int:
354
+ """Length of the full-width track (handle's min edge to max edge).
355
+
356
+ The track spans `[0, extent]` so it meets the container edges; each handle
357
+ still travels `[r, extent - r]` (its center), so ticks placed at the handle
358
+ centers sit `r` inside each end and the handle covers that lead-in/out.
359
+ """
360
+ _, end = self._track_range()
361
+ return end + self._handle_size // 2
362
+
363
+ def _pos_to_value(self, pos: int) -> float:
364
+ start, end = self._track_range()
365
+ if end == start:
366
+ return self._minvalue
367
+ ratio = max(0.0, min(1.0, (pos - start) / (end - start)))
368
+ if self._orient == "vertical":
369
+ ratio = 1.0 - ratio
370
+ return self._minvalue + ratio * (self._maxvalue - self._minvalue)
371
+
372
+ def _event_pos(self, event: tk.Event) -> int:
373
+ # event.x/y are already in canvas coordinates.
374
+ if self._orient == "horizontal":
375
+ return event.x
376
+ return event.y
377
+
378
+ def _snap(self, v: float) -> float:
379
+ """Clamp `v` to [min, max] and, when `step` is set, snap it to the
380
+ nearest grid point (measured from `minvalue`). `maxvalue` stays reachable
381
+ even when the range is not an exact multiple of `step`."""
382
+ v = max(self._minvalue, min(self._maxvalue, float(v)))
383
+ if not self._step_size:
384
+ return v
385
+ grid = self._minvalue + round((v - self._minvalue) / self._step_size) * self._step_size
386
+ grid = max(self._minvalue, min(self._maxvalue, grid))
387
+ if abs(v - self._maxvalue) < abs(v - grid):
388
+ return self._maxvalue
389
+ return grid
390
+
391
+ # ------------------------------------------------------------------
392
+ # Rendering helpers
393
+ # ------------------------------------------------------------------
394
+
395
+ def _render_handles(self, colors: dict[str, str]) -> None:
396
+ fill = colors['handle_disabled'] if self._state == "disabled" else colors['handle']
397
+ self._handle_lo_photo = _make_handle(fill, colors['ring'], colors['handle_border'])
398
+ self._handle_hi_photo = _make_handle(fill, colors['ring'], colors['handle_border'])
399
+ self._canvas.itemconfig(self._handle_lo_item, image=self._handle_lo_photo)
400
+ self._canvas.itemconfig(self._handle_hi_item, image=self._handle_hi_photo)
401
+
402
+ def _update_focus_ring(self) -> None:
403
+ """Position the focus halo over the active handle when the widget holds
404
+ keyboard focus; hide it otherwise."""
405
+ item = getattr(self, "_focus_ring_item", None)
406
+ if item is None:
407
+ return
408
+ if not self._kbd_focused or self._state == "disabled":
409
+ self._canvas.itemconfigure(item, state="hidden")
410
+ return
411
+ val = self._lo_var.get() if self._focus_handle == "lo" else self._hi_var.get()
412
+ pos = self._value_to_pos(val)
413
+ if self._orient == "horizontal":
414
+ self._canvas.coords(item, pos, self._track_center)
415
+ else:
416
+ self._canvas.coords(item, self._track_center, pos)
417
+ self._canvas.itemconfigure(item, state="normal")
418
+ self._canvas.tag_raise(item) # keep the halo above the handles
419
+
420
+ def _update_badge_colors(self, colors: dict[str, str]) -> None:
421
+ bg = colors['badge_bg']
422
+ if self._badge_lo_item is not None:
423
+ self._badge_lo_photo = _make_badge(self._badge_w, BADGE_H, bg)
424
+ self._canvas.itemconfig(self._badge_lo_item, image=self._badge_lo_photo)
425
+ if self._badge_hi_item is not None:
426
+ self._badge_hi_photo = _make_badge(self._badge_w, BADGE_H, bg)
427
+ self._canvas.itemconfig(self._badge_hi_item, image=self._badge_hi_photo)
428
+ for item in (self._badge_lo_text, self._badge_hi_text):
429
+ if item is not None:
430
+ self._canvas.itemconfig(item, fill=colors['badge_text'])
431
+
432
+ # ------------------------------------------------------------------
433
+ # Sync: values → visual
434
+ # ------------------------------------------------------------------
435
+
436
+ def _sync(self) -> None:
437
+ lo_val = self._lo_var.get()
438
+ hi_val = self._hi_var.get()
439
+ lo_pos = self._value_to_pos(lo_val)
440
+ hi_pos = self._value_to_pos(hi_val)
441
+ # Full-width track (0 .. extent); the active fill spans between the handles.
442
+ extent = self._track_extent()
443
+
444
+ fill_c = self._colors['fill_disabled'] if self._state == "disabled" else self._colors['fill']
445
+
446
+ if self._orient == "horizontal":
447
+ photo = _make_track(extent, self._track_h, lo_pos, hi_pos,
448
+ self._colors['trough'], fill_c)
449
+ if photo:
450
+ self._track_photo = photo
451
+ self._canvas.coords(self._track_item, 0, self._track_center)
452
+ self._canvas.itemconfig(self._track_item, image=self._track_photo)
453
+ self._canvas.coords(self._handle_lo_item, lo_pos, self._track_center)
454
+ self._canvas.coords(self._handle_hi_item, hi_pos, self._track_center)
455
+ if self._show_value:
456
+ self._update_badges_h(lo_pos, hi_pos, lo_val, hi_val)
457
+ else:
458
+ # Vertical: top = max (small canvas y), bottom = min (large canvas y).
459
+ # lo_pos >= hi_pos. PIL rotate(90) is CCW (LEFT → BOTTOM), so to fill
460
+ # from canvas y=hi_pos (top) to y=lo_pos (bottom) on the full-height
461
+ # track: fill_start = extent - lo_pos, fill_end = extent - hi_pos.
462
+ fill_start = max(0, extent - lo_pos)
463
+ fill_end = min(extent, extent - hi_pos)
464
+ photo = _make_track(extent, self._track_h, fill_start, fill_end,
465
+ self._colors['trough'], fill_c,
466
+ vertical=True)
467
+ if photo:
468
+ self._track_photo = photo
469
+ self._canvas.coords(self._track_item, self._track_center, 0)
470
+ self._canvas.itemconfig(self._track_item, image=self._track_photo)
471
+ self._canvas.coords(self._handle_lo_item, self._track_center, lo_pos)
472
+ self._canvas.coords(self._handle_hi_item, self._track_center, hi_pos)
473
+ if self._show_value:
474
+ self._update_badges_v(lo_pos, hi_pos, lo_val, hi_val)
475
+
476
+ self._update_focus_ring() # keep the halo on the active handle
477
+
478
+ def _update_badges_h(self, lo_x: int, hi_x: int, lo_val: float, hi_val: float) -> None:
479
+ if self._badge_lo_item is None:
480
+ return
481
+ bw = self._badge_w
482
+ cw = self._cw if self._cw > 0 else max(1, self.winfo_width() - 2 * HL)
483
+ badge_y = self._badge_zone - BADGE_H - 2
484
+ # Clamp so image occupies [bx, bx+bw-1] ⊆ [0, cw-1].
485
+ lo_bx = max(0, min(lo_x - bw // 2, cw - bw))
486
+ hi_bx = max(0, min(hi_x - bw // 2, cw - bw))
487
+ for img_item, text_item, bx, val in (
488
+ (self._badge_lo_item, self._badge_lo_text, lo_bx, lo_val),
489
+ (self._badge_hi_item, self._badge_hi_text, hi_bx, hi_val),
490
+ ):
491
+ self._canvas.coords(img_item, bx, badge_y)
492
+ if text_item is not None:
493
+ self._canvas.coords(text_item, bx + bw // 2, badge_y + BADGE_H // 2)
494
+ self._canvas.itemconfig(text_item, text=self._tick_format.format(val))
495
+
496
+ def _update_badges_v(self, lo_y: int, hi_y: int, lo_val: float, hi_val: float) -> None:
497
+ if self._badge_lo_item is None:
498
+ return
499
+ bw = self._badge_w
500
+ ch = self.winfo_height() - 2 * HL
501
+ badge_x = self._badge_zone - bw - 2
502
+ for img_item, text_item, by_center, val in (
503
+ (self._badge_lo_item, self._badge_lo_text, lo_y, lo_val),
504
+ (self._badge_hi_item, self._badge_hi_text, hi_y, hi_val),
505
+ ):
506
+ badge_y = max(0, min(by_center - BADGE_H // 2, ch - BADGE_H))
507
+ self._canvas.coords(img_item, badge_x, badge_y)
508
+ if text_item is not None:
509
+ self._canvas.coords(text_item, badge_x + bw // 2, badge_y + BADGE_H // 2)
510
+ self._canvas.itemconfig(text_item, text=self._tick_format.format(val))
511
+
512
+ # ------------------------------------------------------------------
513
+ # Tick marks
514
+ # ------------------------------------------------------------------
515
+
516
+ def _setup_ticks(self) -> None:
517
+ if self._tick_interval is None:
518
+ return
519
+ for item in self._tick_items:
520
+ self._canvas.delete(item)
521
+ self._tick_items.clear()
522
+
523
+ tick_color = self._colors['tick']
524
+ font = tkfont.Font(family="TkDefaultFont", size=LABEL_FONT_SIZE)
525
+ start, end = self._track_range()
526
+ span = self._maxvalue - self._minvalue
527
+ if span <= 0:
528
+ return
529
+
530
+ if self._orient == "horizontal":
531
+ major_y0 = self._track_center + self._handle_size // 2 + TICK_GAP
532
+ major_y1 = major_y0 + MAJOR_TICK_H
533
+ minor_y1 = major_y0 + MINOR_TICK_H
534
+ label_y = major_y1 + LABEL_GAP
535
+
536
+ val = self._minvalue
537
+ while val <= self._maxvalue + 1e-9:
538
+ x = self._value_to_pos(val)
539
+ self._tick_items.append(
540
+ self._canvas.create_line(x, major_y0, x, major_y1, fill=tick_color, width=1))
541
+ if self._tick_labels:
542
+ self._tick_items.append(
543
+ self._canvas.create_text(x, label_y, text=self._tick_format.format(val),
544
+ fill=tick_color, font=font, anchor="n"))
545
+ if self._minor_ticks > 0:
546
+ minor_step = self._tick_interval / (self._minor_ticks + 1)
547
+ for i in range(1, self._minor_ticks + 1):
548
+ mv = val + i * minor_step
549
+ if mv >= self._maxvalue - 1e-9:
550
+ break
551
+ mx = self._value_to_pos(mv)
552
+ self._tick_items.append(
553
+ self._canvas.create_line(mx, major_y0, mx, minor_y1,
554
+ fill=tick_color, width=1))
555
+ val += self._tick_interval
556
+ else:
557
+ major_x0 = self._track_center + self._handle_size // 2 + TICK_GAP
558
+ major_x1 = major_x0 + MAJOR_TICK_H
559
+ minor_x1 = major_x0 + MINOR_TICK_H
560
+ label_x = major_x1 + LABEL_GAP
561
+
562
+ val = self._minvalue
563
+ while val <= self._maxvalue + 1e-9:
564
+ y = self._value_to_pos(val)
565
+ self._tick_items.append(
566
+ self._canvas.create_line(major_x0, y, major_x1, y, fill=tick_color, width=1))
567
+ if self._tick_labels:
568
+ self._tick_items.append(
569
+ self._canvas.create_text(label_x, y, text=self._tick_format.format(val),
570
+ fill=tick_color, font=font, anchor="w"))
571
+ if self._minor_ticks > 0:
572
+ minor_step = self._tick_interval / (self._minor_ticks + 1)
573
+ for i in range(1, self._minor_ticks + 1):
574
+ mv = val + i * minor_step
575
+ if mv >= self._maxvalue - 1e-9:
576
+ break
577
+ my = self._value_to_pos(mv)
578
+ self._tick_items.append(
579
+ self._canvas.create_line(major_x0, my, minor_x1, my,
580
+ fill=tick_color, width=1))
581
+ val += self._tick_interval
582
+
583
+ # ------------------------------------------------------------------
584
+ # Event handlers
585
+ # ------------------------------------------------------------------
586
+
587
+ def _on_configure(self, event: tk.Event) -> None:
588
+ if self._orient == "horizontal" and event.width > self._handle_size:
589
+ self._cw = event.width
590
+ self._canvas.configure(scrollregion=(0, 0, event.width, event.height))
591
+ self._setup_ticks()
592
+ self._sync()
593
+ elif self._orient == "vertical" and event.height > self._handle_size:
594
+ self._canvas.configure(scrollregion=(0, 0, event.width, event.height))
595
+ self._setup_ticks()
596
+ self._sync()
597
+
598
+ def _mouse_focus_set(self, _event: tk.Event) -> None:
599
+ self._mouse_pressed = True
600
+ self.focus_set()
601
+
602
+ def _on_focus_in(self, _event: tk.Event) -> None:
603
+ if self._mouse_pressed:
604
+ self._mouse_pressed = False
605
+ return
606
+ self._kbd_focused = True
607
+ # The per-handle halo is the focus indicator — no whole-widget frame ring
608
+ # (that would be redundant with the halo).
609
+ self._update_focus_ring()
610
+
611
+ def _on_focus_out(self, _event: tk.Event) -> None:
612
+ self._kbd_focused = False
613
+ self._update_focus_ring()
614
+
615
+ def _on_canvas_click(self, event: tk.Event) -> None:
616
+ if self._state == "disabled":
617
+ return
618
+ pos = self._event_pos(event)
619
+ lo_pos = self._value_to_pos(self._lo_var.get())
620
+ hi_pos = self._value_to_pos(self._hi_var.get())
621
+ self._dragging = "lo" if abs(pos - lo_pos) <= abs(pos - hi_pos) else "hi"
622
+ self._focus_handle = self._dragging
623
+ self._move(pos)
624
+
625
+ def _on_canvas_drag(self, event: tk.Event) -> None:
626
+ if self._state == "disabled":
627
+ return
628
+ self._move(self._event_pos(event))
629
+
630
+ def _handle_press(self, event: tk.Event, which: str) -> None:
631
+ if self._state == "disabled":
632
+ return
633
+ self._dragging = which
634
+ self._focus_handle = which
635
+ self.focus_set()
636
+
637
+ def _on_handle_drag(self, event: tk.Event) -> None:
638
+ if self._state == "disabled" or self._dragging is None:
639
+ return
640
+ self._move(self._event_pos(event))
641
+
642
+ def _on_release(self, event: tk.Event) -> None:
643
+ if self._state == "disabled":
644
+ return
645
+ self._dragging = None
646
+ self.event_generate("<<Commit>>", data=RangeSliderCommitEvent(
647
+ low_value=self._lo_var.get(),
648
+ high_value=self._hi_var.get(),
649
+ ))
650
+
651
+ def _move(self, pos: int) -> None:
652
+ value = self._snap(self._pos_to_value(pos))
653
+ if self._dragging == "lo":
654
+ self._lo_var.set(max(self._minvalue, min(value, self._hi_var.get())))
655
+ else:
656
+ self._hi_var.set(min(self._maxvalue, max(value, self._lo_var.get())))
657
+
658
+ def _step(self, amount: float) -> None:
659
+ if self._state == "disabled":
660
+ return
661
+ self._set_focused(
662
+ (self._lo_var.get() if self._focus_handle == "lo" else self._hi_var.get()) + amount
663
+ )
664
+ self.event_generate("<<Commit>>", data=RangeSliderCommitEvent(
665
+ low_value=self._lo_var.get(),
666
+ high_value=self._hi_var.get(),
667
+ ))
668
+
669
+ def _set_focused(self, value: float) -> None:
670
+ value = self._snap(value)
671
+ if self._focus_handle == "lo":
672
+ self._lo_var.set(min(value, self._hi_var.get()))
673
+ else:
674
+ self._hi_var.set(max(value, self._lo_var.get()))
675
+
676
+ def _jump(self, value: float) -> None:
677
+ """Keyboard jump (Home/End) — move the focused handle then commit."""
678
+ if self._state == "disabled":
679
+ return
680
+ self._set_focused(value)
681
+ self.event_generate("<<Commit>>", data=RangeSliderCommitEvent(
682
+ low_value=self._lo_var.get(),
683
+ high_value=self._hi_var.get(),
684
+ ))
685
+
686
+ def _reclamp(self) -> None:
687
+ """Pull both handles back into [min, max] (and lo <= hi) after a range
688
+ change, emitting <<Change>> only for handles that actually move."""
689
+ lo, hi = self._lo_var.get(), self._hi_var.get()
690
+ nlo = self._snap(lo)
691
+ nhi = self._snap(hi)
692
+ nlo = min(nlo, nhi)
693
+ changed = False
694
+ if nlo != lo:
695
+ self._lo_var.set(nlo) # trace fires _sync + <<Change>>
696
+ changed = True
697
+ if nhi != hi:
698
+ self._hi_var.set(nhi)
699
+ changed = True
700
+ if not changed:
701
+ self._sync()
702
+
703
+ def _select_handle(self, which: str) -> str:
704
+ """Make `which` ('lo'|'hi') the active handle (cross-axis arrow keys).
705
+
706
+ Re-renders so the focus ring moves to the newly active handle. Returns
707
+ `'break'` so the arrow press isn't reinterpreted by any default binding.
708
+ """
709
+ if self._state == "disabled":
710
+ return "break"
711
+ self._focus_handle = which
712
+ self._update_focus_ring()
713
+ return "break"
714
+
715
+ def _on_lo_write(self, *_: Any) -> None:
716
+ self._sync()
717
+ lo = self._lo_var.get()
718
+ hi = self._hi_var.get()
719
+ self.event_generate("<<Change>>", data=RangeSliderEvent(
720
+ low_value=lo,
721
+ high_value=hi,
722
+ prev_low_value=self._prev_lovalue,
723
+ prev_high_value=self._prev_hivalue,
724
+ ))
725
+ self._prev_lovalue = lo
726
+ self._prev_hivalue = hi
727
+
728
+ def _on_hi_write(self, *_: Any) -> None:
729
+ self._sync()
730
+ lo = self._lo_var.get()
731
+ hi = self._hi_var.get()
732
+ self.event_generate("<<Change>>", data=RangeSliderEvent(
733
+ low_value=lo,
734
+ high_value=hi,
735
+ prev_low_value=self._prev_lovalue,
736
+ prev_high_value=self._prev_hivalue,
737
+ ))
738
+ self._prev_lovalue = lo
739
+ self._prev_hivalue = hi
740
+
741
+ def _on_map(self) -> None:
742
+ if getattr(self, '_theme_update_pending', False):
743
+ self._on_theme_changed()
744
+
745
+ def _bs_apply_theme(self) -> None:
746
+ """Theme walk hook — re-resolve colors and redraw track, handles, badge,
747
+ focus halo, and tick marks + tick labels. The walk owns visibility
748
+ gating, so this always repaints (no early-out).
749
+ """
750
+ self._theme_update_pending = False
751
+ self._colors = resolve_colors(self._accent, self._surface, get_widget_bg(self.master))
752
+ colors = self._colors
753
+ self.configure(
754
+ highlightbackground=colors['bg'],
755
+ highlightcolor=colors['bg'], # reset to hidden; re-shown on next keyboard focus
756
+ )
757
+ self._canvas.configure(bg=colors['bg'])
758
+ self._render_handles(colors)
759
+ self._update_badge_colors(colors)
760
+ # Recolor the focus halo to the new theme's focus color.
761
+ self._focus_ring_photo = _make_focus_ring(self._focus_ring_diam, colors['focus'])
762
+ self._canvas.itemconfig(self._focus_ring_item, image=self._focus_ring_photo)
763
+ self._setup_ticks() # redraws ticks + tick labels with colors['tick']
764
+ self._sync() # _sync repositions/shows the halo as needed
765
+
766
+ def _on_theme_changed(self) -> None:
767
+ # Value/size redraws keep their own off-screen deferral (flushed on
768
+ # <Map>); a genuine theme change is driven through _bs_apply_theme by the
769
+ # unified theme walk.
770
+ if not self.winfo_viewable():
771
+ self._theme_update_pending = True
772
+ return
773
+ self._bs_apply_theme()
774
+
775
+ # ------------------------------------------------------------------
776
+ # Public API
777
+ # ------------------------------------------------------------------
778
+
779
+ @property
780
+ def lo_signal(self) -> Signal[float]:
781
+ """Reactive Signal linked to the low-handle value."""
782
+ return self._lo_signal
783
+
784
+ @property
785
+ def hi_signal(self) -> Signal[float]:
786
+ """Reactive Signal linked to the high-handle value."""
787
+ return self._hi_signal
788
+
789
+ @property
790
+ def lo_variable(self) -> tk.DoubleVar:
791
+ """Underlying Tkinter DoubleVar for the low handle.
792
+
793
+ See [tkinter Variables](https://docs.python.org/3/library/tkinter.html#tkinter-variables).
794
+ """
795
+ return self._lo_var
796
+
797
+ @property
798
+ def hi_variable(self) -> tk.DoubleVar:
799
+ """Underlying Tkinter DoubleVar for the high handle.
800
+
801
+ See [tkinter Variables](https://docs.python.org/3/library/tkinter.html#tkinter-variables).
802
+ """
803
+ return self._hi_var
804
+
805
+ @property
806
+ def lovalue(self) -> float:
807
+ """Current low-handle value."""
808
+ return self._lo_var.get()
809
+
810
+ @property
811
+ def hivalue(self) -> float:
812
+ """Current high-handle value."""
813
+ return self._hi_var.get()
814
+
815
+ def get_lo(self) -> float:
816
+ """Return the current low-handle value."""
817
+ return self._lo_var.get()
818
+
819
+ def get_hi(self) -> float:
820
+ """Return the current high-handle value."""
821
+ return self._hi_var.get()
822
+
823
+ def set_lo(self, value: float) -> None:
824
+ """Set the low handle to a new value, clamped to [minvalue, hivalue] and
825
+ snapped to the step grid when `step` is set.
826
+
827
+ Args:
828
+ value: New low-handle value.
829
+ """
830
+ self._lo_var.set(min(self._snap(value), self._hi_var.get()))
831
+
832
+ def set_hi(self, value: float) -> None:
833
+ """Set the high handle to a new value, clamped to [lovalue, maxvalue] and
834
+ snapped to the step grid when `step` is set.
835
+
836
+ Args:
837
+ value: New high-handle value.
838
+ """
839
+ self._hi_var.set(max(self._snap(value), self._lo_var.get()))
840
+
841
+ def on_changed(self, callback: Callable[[RangeSliderEvent], None]) -> str:
842
+ """Register a callback for `<<Change>>` events.
843
+
844
+ Args:
845
+ callback: Called with a Tkinter event; `event.low_value`
846
+ and `event.high_value` are the current handle values;
847
+ `event.prev_low_value` and `event.prev_high_value`
848
+ are the values before the change.
849
+
850
+ Returns:
851
+ Bind ID — pass to `off_changed()` to unsubscribe.
852
+ """
853
+ return self.bind("<<Change>>", callback, add="+")
854
+
855
+ def off_changed(self, bind_id: str | None = None) -> None:
856
+ """Unsubscribe from `<<Change>>`.
857
+
858
+ Args:
859
+ bind_id: ID returned by `on_changed()`. If None, removes all.
860
+ """
861
+ self.unbind("<<Change>>", bind_id)
862
+
863
+ def on_commit(self, callback: Callable[[RangeSliderCommitEvent], None]) -> str:
864
+ """Register a callback for `<<Commit>>` events.
865
+
866
+ Fires on mouse release and keyboard commit. Use for expensive
867
+ downstream operations.
868
+
869
+ Args:
870
+ callback: Called with a Tkinter event; `event.low_value`
871
+ and `event.high_value` are the committed values.
872
+
873
+ Returns:
874
+ Bind ID — pass to `off_commit()` to unsubscribe.
875
+ """
876
+ return self.bind("<<Commit>>", callback, add="+")
877
+
878
+ def off_commit(self, bind_id: str | None = None) -> None:
879
+ """Unsubscribe from `<<Commit>>`.
880
+
881
+ Args:
882
+ bind_id: ID returned by `on_commit()`. If None, removes all.
883
+ """
884
+ self.unbind("<<Commit>>", bind_id)
885
+
886
+ # ------------------------------------------------------------------
887
+ # Configure delegates
888
+ # ------------------------------------------------------------------
889
+
890
+ @configure_delegate('lovalue')
891
+ def _delegate_lovalue(self, value=None):
892
+ if value is None:
893
+ return self._lo_var.get()
894
+ self.set_lo(value)
895
+
896
+ @configure_delegate('hivalue')
897
+ def _delegate_hivalue(self, value=None):
898
+ if value is None:
899
+ return self._hi_var.get()
900
+ self.set_hi(value)
901
+
902
+ @configure_delegate('minvalue')
903
+ def _delegate_minvalue(self, value=None):
904
+ if value is None:
905
+ return self._minvalue
906
+ self._minvalue = float(value)
907
+ self._setup_ticks()
908
+ self._reclamp()
909
+
910
+ @configure_delegate('maxvalue')
911
+ def _delegate_maxvalue(self, value=None):
912
+ if value is None:
913
+ return self._maxvalue
914
+ self._maxvalue = float(value)
915
+ self._setup_ticks()
916
+ self._reclamp()
917
+
918
+ @configure_delegate('step')
919
+ def _delegate_step(self, value=None):
920
+ if value is None:
921
+ return self._step_size
922
+ self._step_size = float(value) if value else None
923
+ self._kbd_step = self._step_size if self._step_size else float(STEP)
924
+ self._kbd_step_large = self._step_size * 10 if self._step_size else float(STEP_LARGE)
925
+ self._reclamp() # re-snap both handles onto the new grid
926
+
927
+ @configure_delegate('accent')
928
+ def _delegate_accent(self, value=None):
929
+ if value is None:
930
+ return self._accent
931
+ self._accent = value
932
+ self._on_theme_changed()
933
+
934
+ @configure_delegate('state')
935
+ def _delegate_state(self, value=None):
936
+ if value is None:
937
+ return self._state
938
+ self._state = value
939
+ self._render_handles(self._colors)
940
+ self._sync()
941
+
942
+ @configure_delegate('tick_format')
943
+ def _delegate_tick_format(self, value=None):
944
+ if value is None:
945
+ return self._tick_format
946
+ self._tick_format = value
947
+ self._setup_ticks()
948
+ self._sync()
949
+
950
+ @configure_delegate('tick_interval')
951
+ def _delegate_tick_interval(self, value=None):
952
+ if value is None:
953
+ return self._tick_interval
954
+ self._tick_interval = value
955
+ self._setup_ticks()
956
+
957
+ @configure_delegate('show_value')
958
+ def _delegate_show_value(self, value=None):
959
+ if value is None:
960
+ return self._show_value
961
+ # show_value creates/destroys canvas items; must be set at construction time.
962
+
963
+ @configure_delegate('minor_ticks')
964
+ def _delegate_minor_ticks(self, value=None):
965
+ if value is None:
966
+ return self._minor_ticks
967
+ self._minor_ticks = int(value)
968
+ self._setup_ticks()
969
+
970
+ @configure_delegate('tick_labels')
971
+ def _delegate_tick_labels(self, value=None):
972
+ if value is None:
973
+ return self._tick_labels
974
+ self._tick_labels = bool(value)
975
+ self._setup_ticks()
976
+
977
+ @configure_delegate('surface')
978
+ def _delegate_surface(self, value=None):
979
+ if value is None:
980
+ return self._surface
981
+ self._surface = value
982
+ self._on_theme_changed()