bootstack 0.1.0a1__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.
- bootstack/__init__.py +249 -0
- bootstack/__main__.py +5 -0
- bootstack/api/__init__.py +127 -0
- bootstack/api/app.py +30 -0
- bootstack/api/constants.py +3 -0
- bootstack/api/data.py +23 -0
- bootstack/api/dialogs.py +44 -0
- bootstack/api/i18n.py +17 -0
- bootstack/api/localization.py +16 -0
- bootstack/api/menu.py +7 -0
- bootstack/api/style.py +23 -0
- bootstack/api/utils.py +24 -0
- bootstack/api/widgets.py +137 -0
- bootstack/assets/__init__.py +24 -0
- bootstack/assets/bootstack-transparent.png +0 -0
- bootstack/assets/bootstack.ico +0 -0
- bootstack/assets/bootstack.png +0 -0
- bootstack/assets/elements/__init__.py +0 -0
- bootstack/assets/elements/badge-pill.png +0 -0
- bootstack/assets/elements/badge-square.png +0 -0
- bootstack/assets/elements/border.png +0 -0
- bootstack/assets/elements/button-compact.png +0 -0
- bootstack/assets/elements/button-default.png +0 -0
- bootstack/assets/elements/button-group-horizontal-after-compact.png +0 -0
- bootstack/assets/elements/button-group-horizontal-after-default.png +0 -0
- bootstack/assets/elements/button-group-horizontal-before-compact.png +0 -0
- bootstack/assets/elements/button-group-horizontal-before-default.png +0 -0
- bootstack/assets/elements/button-group-horizontal-center-compact.png +0 -0
- bootstack/assets/elements/button-group-horizontal-center-default.png +0 -0
- bootstack/assets/elements/button-group-vertical-after-compact.png +0 -0
- bootstack/assets/elements/button-group-vertical-after-default.png +0 -0
- bootstack/assets/elements/button-group-vertical-before-compact.png +0 -0
- bootstack/assets/elements/button-group-vertical-before-default.png +0 -0
- bootstack/assets/elements/button-group-vertical-center-compact.png +0 -0
- bootstack/assets/elements/button-group-vertical-center-default.png +0 -0
- bootstack/assets/elements/checkbox-checked.png +0 -0
- bootstack/assets/elements/checkbox-indeterminate.png +0 -0
- bootstack/assets/elements/checkbox-unchecked.png +0 -0
- bootstack/assets/elements/field.png +0 -0
- bootstack/assets/elements/input-after-compact.png +0 -0
- bootstack/assets/elements/input-after-default.png +0 -0
- bootstack/assets/elements/input-before-compact.png +0 -0
- bootstack/assets/elements/input-before-default.png +0 -0
- bootstack/assets/elements/input-compact.png +0 -0
- bootstack/assets/elements/input-default.png +0 -0
- bootstack/assets/elements/list-item-separated.png +0 -0
- bootstack/assets/elements/list-item.png +0 -0
- bootstack/assets/elements/manifest.toml +480 -0
- bootstack/assets/elements/menu-item.png +0 -0
- bootstack/assets/elements/nav-button-compact.png +0 -0
- bootstack/assets/elements/nav-button-default.png +0 -0
- bootstack/assets/elements/nav-icon-button-compact.png +0 -0
- bootstack/assets/elements/nav-icon-button-default.png +0 -0
- bootstack/assets/elements/notebook-client-border.png +0 -0
- bootstack/assets/elements/notebook-tab-active.png +0 -0
- bootstack/assets/elements/notebook-tab-bar.png +0 -0
- bootstack/assets/elements/notebook-tab-normal.png +0 -0
- bootstack/assets/elements/notebook-tab-pill.png +0 -0
- bootstack/assets/elements/progress-bar-horizontal-striped.png +0 -0
- bootstack/assets/elements/progress-bar-solid.png +0 -0
- bootstack/assets/elements/progress-bar-thin.png +0 -0
- bootstack/assets/elements/progress-bar-vertical-striped.png +0 -0
- bootstack/assets/elements/radio-selected.png +0 -0
- bootstack/assets/elements/radio-unselected.png +0 -0
- bootstack/assets/elements/scrollbar-horizontal.png +0 -0
- bootstack/assets/elements/scrollbar-vertical.png +0 -0
- bootstack/assets/elements/slider-handle-focus.png +0 -0
- bootstack/assets/elements/slider-handle.png +0 -0
- bootstack/assets/elements/slider-track-horizontal.png +0 -0
- bootstack/assets/elements/slider-track-vertical.png +0 -0
- bootstack/assets/elements/switch-off.png +0 -0
- bootstack/assets/elements/switch-on.png +0 -0
- bootstack/assets/elements/tabs-bar-horizontal.png +0 -0
- bootstack/assets/elements/tabs-bar-vertical.png +0 -0
- bootstack/assets/elements/tabs-pill.png +0 -0
- bootstack/assets/locales/ar/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/ar/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/bg/LC_MESSAGES/bootstack.po +875 -0
- bootstack/assets/locales/cs/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/cs/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/da/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/da/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/de/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/de/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/en/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/en/LC_MESSAGES/bootstack.po +875 -0
- bootstack/assets/locales/es/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/es/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/fr/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/fr/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/he/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/he/LC_MESSAGES/bootstack.po +851 -0
- bootstack/assets/locales/hi/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/hi/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/it/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/it/LC_MESSAGES/bootstack.po +841 -0
- bootstack/assets/locales/ja/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/ja/LC_MESSAGES/bootstack.po +914 -0
- bootstack/assets/locales/ko/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/ko/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/nb/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/nb/LC_MESSAGES/bootstack.po +841 -0
- bootstack/assets/locales/nl/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/nl/LC_MESSAGES/bootstack.po +841 -0
- bootstack/assets/locales/pl/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/pl/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/pt/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/pt/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/sl/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/sl/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/sv/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/sv/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/tr/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/tr/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/themes/__init__.py +0 -0
- bootstack/assets/themes/amber-dark.json +32 -0
- bootstack/assets/themes/amber-light.json +32 -0
- bootstack/assets/themes/aurora-dark.json +32 -0
- bootstack/assets/themes/aurora-light.json +32 -0
- bootstack/assets/themes/bootstrap-dark.json +32 -0
- bootstack/assets/themes/bootstrap-light.json +32 -0
- bootstack/assets/themes/classic-dark.json +32 -0
- bootstack/assets/themes/classic-light.json +32 -0
- bootstack/assets/themes/docs-dark.json +32 -0
- bootstack/assets/themes/docs-light.json +32 -0
- bootstack/assets/themes/forest-dark.json +32 -0
- bootstack/assets/themes/forest-light.json +32 -0
- bootstack/assets/themes/ocean-dark.json +32 -0
- bootstack/assets/themes/ocean-light.json +32 -0
- bootstack/assets/themes/rose-dark.json +32 -0
- bootstack/assets/themes/rose-light.json +32 -0
- bootstack/assets/widgets/__init__.py +0 -0
- bootstack/assets/widgets/badge-default.png +0 -0
- bootstack/assets/widgets/badge-pill.png +0 -0
- bootstack/assets/widgets/border.png +0 -0
- bootstack/assets/widgets/button-group-horizontal-after.png +0 -0
- bootstack/assets/widgets/button-group-horizontal-before.png +0 -0
- bootstack/assets/widgets/button-group-horizontal-center.png +0 -0
- bootstack/assets/widgets/button-group-vertical-after.png +0 -0
- bootstack/assets/widgets/button-group-vertical-before.png +0 -0
- bootstack/assets/widgets/button-group-vertical-center.png +0 -0
- bootstack/assets/widgets/button.png +0 -0
- bootstack/assets/widgets/checkbox-checked.png +0 -0
- bootstack/assets/widgets/checkbox-indeterminate.png +0 -0
- bootstack/assets/widgets/checkbox-unchecked.png +0 -0
- bootstack/assets/widgets/field.png +0 -0
- bootstack/assets/widgets/icon-button.png +0 -0
- bootstack/assets/widgets/input-inner.png +0 -0
- bootstack/assets/widgets/input-prefix.png +0 -0
- bootstack/assets/widgets/input-suffix.png +0 -0
- bootstack/assets/widgets/input.png +0 -0
- bootstack/assets/widgets/list-item-focus.png +0 -0
- bootstack/assets/widgets/list-item-separated.png +0 -0
- bootstack/assets/widgets/menu-item-separated.png +0 -0
- bootstack/assets/widgets/notebook-client-border.png +0 -0
- bootstack/assets/widgets/notebook-pill-active.png +0 -0
- bootstack/assets/widgets/notebook-pill-inactive.png +0 -0
- bootstack/assets/widgets/notebook-tab-active.png +0 -0
- bootstack/assets/widgets/notebook-tab-border.png +0 -0
- bootstack/assets/widgets/notebook-tab-normal.png +0 -0
- bootstack/assets/widgets/notebook-underline.png +0 -0
- bootstack/assets/widgets/progress-bar-horizontal-default.png +0 -0
- bootstack/assets/widgets/progress-bar-horizontal-striped.png +0 -0
- bootstack/assets/widgets/progress-bar-vertical-default.png +0 -0
- bootstack/assets/widgets/progress-bar-vertical-striped.png +0 -0
- bootstack/assets/widgets/progress-trough-horizontal.png +0 -0
- bootstack/assets/widgets/progress-trough-vertical.png +0 -0
- bootstack/assets/widgets/radio-selected.png +0 -0
- bootstack/assets/widgets/radio-unselected.png +0 -0
- bootstack/assets/widgets/scrollbar-horizontal-rounded.png +0 -0
- bootstack/assets/widgets/scrollbar-vertical-rounded.png +0 -0
- bootstack/assets/widgets/separator-horizontal.png +0 -0
- bootstack/assets/widgets/separator-vertical.png +0 -0
- bootstack/assets/widgets/slider-handle-focus.png +0 -0
- bootstack/assets/widgets/slider-handle.png +0 -0
- bootstack/assets/widgets/slider-track-horizontal.png +0 -0
- bootstack/assets/widgets/slider-track-vertical.png +0 -0
- bootstack/assets/widgets/switch-off.png +0 -0
- bootstack/assets/widgets/switch-on.png +0 -0
- bootstack/assets/widgets/tabs-bar-horizontal.png +0 -0
- bootstack/assets/widgets/tabs-bar-vertical.png +0 -0
- bootstack/assets/widgets/tabs-pill.png +0 -0
- bootstack/cli/__init__.py +124 -0
- bootstack/cli/__main__.py +6 -0
- bootstack/cli/add.py +439 -0
- bootstack/cli/build.py +115 -0
- bootstack/cli/config.py +287 -0
- bootstack/cli/demo.py +1267 -0
- bootstack/cli/doctor.py +195 -0
- bootstack/cli/list_cmd.py +71 -0
- bootstack/cli/promote.py +120 -0
- bootstack/cli/pyinstaller.py +246 -0
- bootstack/cli/run.py +99 -0
- bootstack/cli/start.py +105 -0
- bootstack/cli/templates/__init__.py +861 -0
- bootstack/constants.py +325 -0
- bootstack/core/__init__.py +34 -0
- bootstack/core/capabilities/__init__.py +45 -0
- bootstack/core/capabilities/after.py +103 -0
- bootstack/core/capabilities/bind.py +154 -0
- bootstack/core/capabilities/bindtags.py +112 -0
- bootstack/core/capabilities/busy.py +61 -0
- bootstack/core/capabilities/clipboard.py +88 -0
- bootstack/core/capabilities/focus.py +118 -0
- bootstack/core/capabilities/grab.py +65 -0
- bootstack/core/capabilities/grid.py +188 -0
- bootstack/core/capabilities/localization.py +231 -0
- bootstack/core/capabilities/pack.py +119 -0
- bootstack/core/capabilities/place.py +92 -0
- bootstack/core/capabilities/selection.py +136 -0
- bootstack/core/capabilities/signals.py +242 -0
- bootstack/core/capabilities/winfo.py +315 -0
- bootstack/core/colorutils.py +234 -0
- bootstack/core/exceptions.py +95 -0
- bootstack/core/images.py +283 -0
- bootstack/core/localization/README.md +90 -0
- bootstack/core/localization/__init__.py +13 -0
- bootstack/core/localization/intl_format.py +580 -0
- bootstack/core/localization/msgcat.py +425 -0
- bootstack/core/localization/specs.py +143 -0
- bootstack/core/mixins/__init__.py +1 -0
- bootstack/core/mixins/ttk_state.py +35 -0
- bootstack/core/mixins/widget.py +132 -0
- bootstack/core/publisher.py +147 -0
- bootstack/core/signals/README.md +112 -0
- bootstack/core/signals/__init__.py +8 -0
- bootstack/core/signals/integration.py +100 -0
- bootstack/core/signals/signal.py +317 -0
- bootstack/core/signals/types.py +4 -0
- bootstack/core/validation/__init__.py +5 -0
- bootstack/core/validation/types.py +13 -0
- bootstack/core/validation/validation_result.py +17 -0
- bootstack/core/validation/validation_rules.py +112 -0
- bootstack/core/variables.py +62 -0
- bootstack/datasource/README.md +607 -0
- bootstack/datasource/__init__.py +51 -0
- bootstack/datasource/base.py +474 -0
- bootstack/datasource/file_source.py +541 -0
- bootstack/datasource/memory_source.py +482 -0
- bootstack/datasource/sqlite_source.py +453 -0
- bootstack/datasource/types.py +259 -0
- bootstack/dialogs/__init__.py +56 -0
- bootstack/dialogs/colorchooser.py +674 -0
- bootstack/dialogs/colordropper.py +257 -0
- bootstack/dialogs/datedialog.py +404 -0
- bootstack/dialogs/dialog.py +514 -0
- bootstack/dialogs/filterdialog.py +358 -0
- bootstack/dialogs/fontdialog.py +339 -0
- bootstack/dialogs/formdialog.py +541 -0
- bootstack/dialogs/message.py +489 -0
- bootstack/dialogs/query.py +561 -0
- bootstack/py.typed +1 -0
- bootstack/runtime/__init__.py +3 -0
- bootstack/runtime/app.py +879 -0
- bootstack/runtime/base_window.py +786 -0
- bootstack/runtime/events.py +399 -0
- bootstack/runtime/menu.py +510 -0
- bootstack/runtime/shortcuts.py +423 -0
- bootstack/runtime/tk_patch.py +31 -0
- bootstack/runtime/toplevel.py +131 -0
- bootstack/runtime/utility.py +371 -0
- bootstack/runtime/visual_focus.py +228 -0
- bootstack/runtime/window_utilities.py +1043 -0
- bootstack/style/__init__.py +5498 -0
- bootstack/style/bootstyle.py +507 -0
- bootstack/style/bootstyle_builder_base.py +752 -0
- bootstack/style/bootstyle_builder_mixed.py +93 -0
- bootstack/style/bootstyle_builder_tk.py +109 -0
- bootstack/style/bootstyle_builder_ttk.py +354 -0
- bootstack/style/builders/__init__.py +51 -0
- bootstack/style/builders/badge.py +44 -0
- bootstack/style/builders/button.py +453 -0
- bootstack/style/builders/buttongroup.py +344 -0
- bootstack/style/builders/calendar.py +271 -0
- bootstack/style/builders/checkbutton.py +95 -0
- bootstack/style/builders/combobox.py +112 -0
- bootstack/style/builders/contextmenu.py +268 -0
- bootstack/style/builders/entry.py +83 -0
- bootstack/style/builders/expander.py +171 -0
- bootstack/style/builders/field.py +312 -0
- bootstack/style/builders/frame.py +27 -0
- bootstack/style/builders/label.py +28 -0
- bootstack/style/builders/labelframe.py +41 -0
- bootstack/style/builders/listview.py +267 -0
- bootstack/style/builders/menubar.py +74 -0
- bootstack/style/builders/menubutton.py +408 -0
- bootstack/style/builders/notebook.py +316 -0
- bootstack/style/builders/panedwindow.py +25 -0
- bootstack/style/builders/progressbar.py +71 -0
- bootstack/style/builders/radiobutton.py +68 -0
- bootstack/style/builders/scale.py +66 -0
- bootstack/style/builders/scrollbar.py +360 -0
- bootstack/style/builders/separator.py +45 -0
- bootstack/style/builders/sidenav.py +313 -0
- bootstack/style/builders/sizegrip.py +15 -0
- bootstack/style/builders/spinbox.py +119 -0
- bootstack/style/builders/switch.py +67 -0
- bootstack/style/builders/tabitem.py +205 -0
- bootstack/style/builders/toolbutton.py +260 -0
- bootstack/style/builders/tooltip.py +26 -0
- bootstack/style/builders/treeview.py +269 -0
- bootstack/style/builders/utils.py +404 -0
- bootstack/style/builders_tk/__init__.py +16 -0
- bootstack/style/builders_tk/defaults.py +229 -0
- bootstack/style/element.py +173 -0
- bootstack/style/style.py +499 -0
- bootstack/style/theme_provider.py +449 -0
- bootstack/style/tk_patch.py +5 -0
- bootstack/style/token_maps.py +42 -0
- bootstack/style/types.py +32 -0
- bootstack/style/typography.py +527 -0
- bootstack/style/utility.py +696 -0
- bootstack/themes/__init__.py +12 -0
- bootstack/themes/standard.py +415 -0
- bootstack/themes/user.py +45 -0
- bootstack/widgets/__init__.py +53 -0
- bootstack/widgets/composites/__init__.py +38 -0
- bootstack/widgets/composites/accordion.py +385 -0
- bootstack/widgets/composites/appshell.py +445 -0
- bootstack/widgets/composites/buttongroup.py +391 -0
- bootstack/widgets/composites/calendar.py +914 -0
- bootstack/widgets/composites/compositeframe.py +282 -0
- bootstack/widgets/composites/contextmenu.py +1754 -0
- bootstack/widgets/composites/dateentry.py +261 -0
- bootstack/widgets/composites/dropdownbutton.py +190 -0
- bootstack/widgets/composites/expander.py +508 -0
- bootstack/widgets/composites/field.py +448 -0
- bootstack/widgets/composites/floodgauge.py +434 -0
- bootstack/widgets/composites/form.py +983 -0
- bootstack/widgets/composites/labeledscale.py +209 -0
- bootstack/widgets/composites/list/__init__.py +15 -0
- bootstack/widgets/composites/list/listitem.py +733 -0
- bootstack/widgets/composites/list/listview.py +1507 -0
- bootstack/widgets/composites/menubar.py +303 -0
- bootstack/widgets/composites/meter.py +882 -0
- bootstack/widgets/composites/numericentry.py +183 -0
- bootstack/widgets/composites/pagestack.py +330 -0
- bootstack/widgets/composites/passwordentry.py +149 -0
- bootstack/widgets/composites/pathentry.py +223 -0
- bootstack/widgets/composites/radiogroup.py +466 -0
- bootstack/widgets/composites/scrolledtext.py +388 -0
- bootstack/widgets/composites/scrolledtext.pyi +186 -0
- bootstack/widgets/composites/scrollview.py +675 -0
- bootstack/widgets/composites/selectbox.py +544 -0
- bootstack/widgets/composites/sidenav/__init__.py +24 -0
- bootstack/widgets/composites/sidenav/group.py +485 -0
- bootstack/widgets/composites/sidenav/header.py +83 -0
- bootstack/widgets/composites/sidenav/item.py +413 -0
- bootstack/widgets/composites/sidenav/separator.py +51 -0
- bootstack/widgets/composites/sidenav/view.py +919 -0
- bootstack/widgets/composites/spinnerentry.py +232 -0
- bootstack/widgets/composites/tableview/__init__.py +5 -0
- bootstack/widgets/composites/tableview/tableview.py +2254 -0
- bootstack/widgets/composites/tableview/types.py +169 -0
- bootstack/widgets/composites/tabs/__init__.py +6 -0
- bootstack/widgets/composites/tabs/tabitem.py +372 -0
- bootstack/widgets/composites/tabs/tabs.py +478 -0
- bootstack/widgets/composites/tabs/tabview.py +352 -0
- bootstack/widgets/composites/textentry.py +90 -0
- bootstack/widgets/composites/timeentry.py +189 -0
- bootstack/widgets/composites/toast.py +364 -0
- bootstack/widgets/composites/togglegroup.py +382 -0
- bootstack/widgets/composites/toolbar.py +393 -0
- bootstack/widgets/composites/tooltip.py +404 -0
- bootstack/widgets/internal/__init__.py +0 -0
- bootstack/widgets/internal/wrapper_base.py +304 -0
- bootstack/widgets/mixins/__init__.py +25 -0
- bootstack/widgets/mixins/configure_mixin.py +186 -0
- bootstack/widgets/mixins/entry_mixin.py +70 -0
- bootstack/widgets/mixins/font_mixin.py +346 -0
- bootstack/widgets/mixins/icon_mixin.py +38 -0
- bootstack/widgets/mixins/localization_mixin.py +255 -0
- bootstack/widgets/mixins/signal_mixin.py +272 -0
- bootstack/widgets/mixins/validation_mixin.py +204 -0
- bootstack/widgets/parts/__init__.py +11 -0
- bootstack/widgets/parts/numberentry_part.py +345 -0
- bootstack/widgets/parts/spinnerentry_part.py +394 -0
- bootstack/widgets/parts/textentry_part.py +344 -0
- bootstack/widgets/primitives/__init__.py +55 -0
- bootstack/widgets/primitives/badge.py +44 -0
- bootstack/widgets/primitives/button.py +89 -0
- bootstack/widgets/primitives/card.py +66 -0
- bootstack/widgets/primitives/checkbutton.py +124 -0
- bootstack/widgets/primitives/checktoggle.py +53 -0
- bootstack/widgets/primitives/combobox.py +165 -0
- bootstack/widgets/primitives/entry.py +98 -0
- bootstack/widgets/primitives/frame.py +206 -0
- bootstack/widgets/primitives/gridframe.py +479 -0
- bootstack/widgets/primitives/label.py +95 -0
- bootstack/widgets/primitives/labelframe.py +63 -0
- bootstack/widgets/primitives/menubutton.py +118 -0
- bootstack/widgets/primitives/notebook.py +551 -0
- bootstack/widgets/primitives/optionmenu.py +248 -0
- bootstack/widgets/primitives/packframe.py +228 -0
- bootstack/widgets/primitives/panedwindow.py +58 -0
- bootstack/widgets/primitives/progressbar.py +95 -0
- bootstack/widgets/primitives/radiobutton.py +115 -0
- bootstack/widgets/primitives/radiotoggle.py +50 -0
- bootstack/widgets/primitives/scale.py +85 -0
- bootstack/widgets/primitives/scrollbar.py +56 -0
- bootstack/widgets/primitives/separator.py +56 -0
- bootstack/widgets/primitives/sizegrip.py +47 -0
- bootstack/widgets/primitives/spinbox.py +91 -0
- bootstack/widgets/primitives/switch.py +41 -0
- bootstack/widgets/primitives/treeview.py +77 -0
- bootstack/widgets/types.py +20 -0
- bootstack-0.1.0a1.dist-info/METADATA +196 -0
- bootstack-0.1.0a1.dist-info/RECORD +419 -0
- bootstack-0.1.0a1.dist-info/WHEEL +5 -0
- bootstack-0.1.0a1.dist-info/entry_points.txt +2 -0
- bootstack-0.1.0a1.dist-info/licenses/LICENSE +22 -0
- bootstack-0.1.0a1.dist-info/licenses/NOTICE +10 -0
- bootstack-0.1.0a1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from typing import List, Optional, Tuple, Union
|
|
6
|
+
|
|
7
|
+
from typing_extensions import Any, TypedDict
|
|
8
|
+
|
|
9
|
+
from ttkbootstrap_icons_bs import BootstrapIcon
|
|
10
|
+
from ttkbootstrap_icons_bs.provider import BootstrapFontProvider
|
|
11
|
+
from ttkbootstrap_icons.providers import BaseFontProvider
|
|
12
|
+
from ttkbootstrap_icons.icon import Icon
|
|
13
|
+
|
|
14
|
+
# Custom y_bias for better vertical alignment in compound buttons.
|
|
15
|
+
# Default Bootstrap provider uses 0.02.
|
|
16
|
+
_ICON_Y_BIAS = 0.02
|
|
17
|
+
_icon_provider_initialized = False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _TtkBootstrapIconProvider(BootstrapFontProvider):
|
|
21
|
+
"""Custom Bootstrap icon provider with adjusted y_bias for bootstack.
|
|
22
|
+
|
|
23
|
+
The default Bootstrap provider uses y_bias=0.02, but bootstack buttons
|
|
24
|
+
need y_bias=0.08 for proper vertical alignment of icons with text.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self):
|
|
28
|
+
# Bypass BootstrapFontProvider.__init__ and call BaseFontProvider directly
|
|
29
|
+
# to allow setting a custom y_bias value
|
|
30
|
+
BaseFontProvider.__init__(
|
|
31
|
+
self,
|
|
32
|
+
name="bootstrap",
|
|
33
|
+
display_name="Bootstrap Icons",
|
|
34
|
+
package="ttkbootstrap_icons_bs.assets",
|
|
35
|
+
homepage="https://icons.getbootstrap.com/",
|
|
36
|
+
license_url="https://github.com/twbs/icons/blob/main/LICENSE",
|
|
37
|
+
icon_version="1.13.1",
|
|
38
|
+
default_style="outline",
|
|
39
|
+
y_bias=_ICON_Y_BIAS,
|
|
40
|
+
styles={
|
|
41
|
+
"fill": {"filename": "bootstrap.ttf", "predicate": BootstrapFontProvider._is_fill_style},
|
|
42
|
+
"outline": {"filename": "bootstrap.ttf", "predicate": BootstrapFontProvider._is_outline_style},
|
|
43
|
+
}
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _ensure_icon_provider():
|
|
48
|
+
"""Initialize the custom icon provider if not already done."""
|
|
49
|
+
global _icon_provider_initialized
|
|
50
|
+
if _icon_provider_initialized:
|
|
51
|
+
return
|
|
52
|
+
provider = _TtkBootstrapIconProvider()
|
|
53
|
+
Icon.initialize_with_provider(provider)
|
|
54
|
+
_icon_provider_initialized = True
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
from bootstack.style.theme_provider import ThemeProvider, use_theme
|
|
58
|
+
from bootstack.style.utility import best_foreground, color_to_hsl, darken_color, lighten_color, mix_colors, \
|
|
59
|
+
relative_luminance
|
|
60
|
+
from bootstack.runtime.utility import scale_size
|
|
61
|
+
|
|
62
|
+
# Source images are created at this resolution multiplier (e.g., 2x)
|
|
63
|
+
# This allows measurements from source images to be used directly in code
|
|
64
|
+
SOURCE_RESOLUTION = 2.0
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class IconSpec(TypedDict, total=False):
|
|
68
|
+
name: str
|
|
69
|
+
size: Optional[int]
|
|
70
|
+
color: Optional[str]
|
|
71
|
+
state: Sequence[tuple[str, str | IconStateMap]]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class IconStateMap(TypedDict, total=False):
|
|
75
|
+
name: Optional[str]
|
|
76
|
+
color: Optional[str]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
ForegroundStateSpec = tuple[str, str | dict[str, str]]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class BootstyleBuilderBase:
|
|
83
|
+
"""Shared base for TTK and Tk bootstyle builders.
|
|
84
|
+
|
|
85
|
+
Centralizes theme provider plumbing and color utilities so both
|
|
86
|
+
BootstyleBuilder (TTK) and BootstyleBuilderTk (Tk) can inherit and
|
|
87
|
+
avoid duplication.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
_RE_TOKEN = re.compile(r"^\s*(?P<head>[a-zA-Z_][\w-]*)(?P<brackets>(?:\[[^]]*])*)\s*$")
|
|
91
|
+
|
|
92
|
+
def __init__(self, theme_provider: ThemeProvider | None = None, style_instance: Any | None = None): # noqa: ANN401
|
|
93
|
+
# If no provider given, try to derive from style_instance
|
|
94
|
+
if theme_provider is None and style_instance is not None:
|
|
95
|
+
try:
|
|
96
|
+
theme_provider = style_instance.theme_provider # type: ignore[attr-defined]
|
|
97
|
+
except Exception:
|
|
98
|
+
theme_provider = None
|
|
99
|
+
self._provider = theme_provider or use_theme()
|
|
100
|
+
self._style = style_instance
|
|
101
|
+
|
|
102
|
+
def set_style_instance(self, style_instance: Any) -> None: # noqa: ANN401
|
|
103
|
+
self._style = style_instance
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def provider(self) -> ThemeProvider:
|
|
107
|
+
return self._provider
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def style(self) -> Any | None: # noqa: ANN401
|
|
111
|
+
return self._style
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def colors(self) -> dict:
|
|
115
|
+
return self.provider.colors
|
|
116
|
+
|
|
117
|
+
# ----- Color Utilities & Transformers -----
|
|
118
|
+
|
|
119
|
+
def _parse_color_token(self, token: str):
|
|
120
|
+
"""Parse a color token into head and ordered list of modifiers.
|
|
121
|
+
|
|
122
|
+
Returns a dict with:
|
|
123
|
+
- head: The base color name (e.g., "primary")
|
|
124
|
+
- modifiers: List of (type, value) tuples in order
|
|
125
|
+
"""
|
|
126
|
+
m = self._RE_TOKEN.match(token)
|
|
127
|
+
if not m:
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
head = m.group("head")
|
|
131
|
+
brackets_raw = m.group("brackets")
|
|
132
|
+
|
|
133
|
+
modifiers = [] # List of (type, value) tuples in order
|
|
134
|
+
|
|
135
|
+
if brackets_raw:
|
|
136
|
+
# Extract all bracket contents: [content1][content2] -> ["content1", "content2"]
|
|
137
|
+
bracket_contents = re.findall(r'\[([^]]*)]', brackets_raw)
|
|
138
|
+
|
|
139
|
+
# Process each bracket in order for pipeline
|
|
140
|
+
for bracket in bracket_contents:
|
|
141
|
+
part = bracket.strip().lower()
|
|
142
|
+
if not part:
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
if part.isdigit():
|
|
146
|
+
modifiers.append(("shade", int(part)))
|
|
147
|
+
elif re.fullmatch(r'[+-]\s*\d+', part):
|
|
148
|
+
rel = int(part.replace(" ", ""))
|
|
149
|
+
modifiers.append(("elevation", rel))
|
|
150
|
+
elif part == "subtle":
|
|
151
|
+
modifiers.append(("subtle", None))
|
|
152
|
+
elif part == "muted":
|
|
153
|
+
modifiers.append(("muted", None))
|
|
154
|
+
|
|
155
|
+
return {"head": head, "modifiers": modifiers}
|
|
156
|
+
|
|
157
|
+
def color(self, token: str, surface: str | None = None, role: str = "background") -> str:
|
|
158
|
+
"""Resolve a color token with optional chained modifiers.
|
|
159
|
+
|
|
160
|
+
Modifiers are applied as a pipeline from left to right:
|
|
161
|
+
- primary[100][muted] → lookup primary[100], then apply muted
|
|
162
|
+
- background[+1][muted] → lookup background, elevate it, then apply muted
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
token: Color token (e.g., "primary", "primary[100][muted]")
|
|
166
|
+
Can also be a hex color string (e.g., "#ff0000")
|
|
167
|
+
surface: Optional surface color for subtle modifier
|
|
168
|
+
role: Role for subtle modifier ("background" or "text")
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
The resolved color value as a hex string
|
|
172
|
+
|
|
173
|
+
Raises:
|
|
174
|
+
ValueError: If the color token cannot be resolved to a valid color.
|
|
175
|
+
"""
|
|
176
|
+
# If token is already a hex color, return it directly
|
|
177
|
+
if token and token.startswith('#'):
|
|
178
|
+
return token
|
|
179
|
+
|
|
180
|
+
# Fast path: exact key (e.g., "blue[100]" or "primary")
|
|
181
|
+
direct = self.colors.get(token)
|
|
182
|
+
if direct is not None:
|
|
183
|
+
return direct
|
|
184
|
+
|
|
185
|
+
parsed = self._parse_color_token(token)
|
|
186
|
+
if not parsed:
|
|
187
|
+
result = self.colors.get(token)
|
|
188
|
+
if result is None:
|
|
189
|
+
raise ValueError(
|
|
190
|
+
f"Invalid color token: '{token}'. "
|
|
191
|
+
f"Valid color tokens include: primary, secondary, success, info, warning, danger, "
|
|
192
|
+
f"light, dark, background, foreground, or a hex color like '#ff0000'."
|
|
193
|
+
)
|
|
194
|
+
return result
|
|
195
|
+
|
|
196
|
+
head = parsed["head"]
|
|
197
|
+
modifiers = parsed["modifiers"]
|
|
198
|
+
|
|
199
|
+
# Determine base lookup key
|
|
200
|
+
# If first modifier is a shade, use it for lookup and remove from pipeline
|
|
201
|
+
base_key = head
|
|
202
|
+
if modifiers and modifiers[0][0] == "shade":
|
|
203
|
+
shade_value = modifiers[0][1]
|
|
204
|
+
base_key = f"{head}[{shade_value}]"
|
|
205
|
+
modifiers = modifiers[1:] # Remove shade from pipeline
|
|
206
|
+
|
|
207
|
+
# Get the base color
|
|
208
|
+
current_color = self.colors.get(base_key)
|
|
209
|
+
if current_color is None:
|
|
210
|
+
# Unknown base; fall back
|
|
211
|
+
result = self.colors.get(token)
|
|
212
|
+
if result is None:
|
|
213
|
+
raise ValueError(
|
|
214
|
+
f"Invalid color token: '{token}'. "
|
|
215
|
+
f"Valid color tokens include: primary, secondary, success, info, warning, danger, "
|
|
216
|
+
f"light, dark, background, foreground, or a hex color like '#ff0000'."
|
|
217
|
+
)
|
|
218
|
+
return result
|
|
219
|
+
|
|
220
|
+
# Apply each modifier in order as a pipeline transformation
|
|
221
|
+
for mod_type, mod_value in modifiers:
|
|
222
|
+
if mod_type == "elevation":
|
|
223
|
+
current_color = self.elevate(current_color, mod_value)
|
|
224
|
+
elif mod_type == "subtle":
|
|
225
|
+
# Subtle needs special handling - it does its own lookup
|
|
226
|
+
# Use the base_key for lookup, not the current transformed color
|
|
227
|
+
current_color = self.subtle(base_key, surface, role)
|
|
228
|
+
elif mod_type == "muted":
|
|
229
|
+
# Muted transforms whatever color we currently have
|
|
230
|
+
current_color = self.muted_foreground(current_color)
|
|
231
|
+
|
|
232
|
+
return current_color
|
|
233
|
+
|
|
234
|
+
def subtle(self, token: str, surface: str | None = None, role: str = "background") -> str:
|
|
235
|
+
"""Return a subtle instance of this color for background or text."""
|
|
236
|
+
# Parse token to handle compound tokens like 'primary[subtle]' or 'primary[100]'
|
|
237
|
+
parsed = self._parse_color_token(token)
|
|
238
|
+
if parsed:
|
|
239
|
+
base_key = parsed["head"]
|
|
240
|
+
# If first modifier is a shade, include it in the lookup key
|
|
241
|
+
modifiers = parsed["modifiers"]
|
|
242
|
+
if modifiers and modifiers[0][0] == "shade":
|
|
243
|
+
base_key = f"{base_key}[{modifiers[0][1]}]"
|
|
244
|
+
base_color = self.colors.get(base_key)
|
|
245
|
+
else:
|
|
246
|
+
base_color = self.colors.get(token)
|
|
247
|
+
|
|
248
|
+
# Fallback if lookup failed
|
|
249
|
+
if base_color is None:
|
|
250
|
+
base_color = self.colors.get(token) or self.colors.get('foreground')
|
|
251
|
+
|
|
252
|
+
surface_val = surface or self.colors.get('background')
|
|
253
|
+
|
|
254
|
+
if role == "text":
|
|
255
|
+
if self.provider.mode == "light":
|
|
256
|
+
return darken_color(base_color, 0.25)
|
|
257
|
+
else:
|
|
258
|
+
return lighten_color(base_color, 0.25)
|
|
259
|
+
else: # background
|
|
260
|
+
if self.provider.mode == "light":
|
|
261
|
+
return mix_colors(base_color, surface_val, 0.08)
|
|
262
|
+
else:
|
|
263
|
+
return mix_colors(base_color, surface_val, 0.10)
|
|
264
|
+
|
|
265
|
+
def active(self, color: str) -> str:
|
|
266
|
+
return self._state_color(color, "active")
|
|
267
|
+
|
|
268
|
+
def pressed(self, color: str) -> str:
|
|
269
|
+
return self._state_color(color, "pressed")
|
|
270
|
+
|
|
271
|
+
def focus(self, color: str) -> str:
|
|
272
|
+
return self._state_color(color, "focus")
|
|
273
|
+
|
|
274
|
+
def selected(self, color: str) -> str:
|
|
275
|
+
return self._state_color(color, "selected")
|
|
276
|
+
|
|
277
|
+
def focus_border(self, color: str) -> str:
|
|
278
|
+
lum = relative_luminance(color)
|
|
279
|
+
if self.provider.mode == "dark":
|
|
280
|
+
return lighten_color(color, 0.1)
|
|
281
|
+
else:
|
|
282
|
+
return darken_color(color, 0.2 if lum > 0.5 else 0.1)
|
|
283
|
+
|
|
284
|
+
def focus_ring(self, color: str, surface: str | None = None) -> str:
|
|
285
|
+
surface = surface or self.color(color)
|
|
286
|
+
lum = relative_luminance(color)
|
|
287
|
+
if self.provider.mode == "dark":
|
|
288
|
+
if lum < 0.3:
|
|
289
|
+
brightened = lighten_color(color, 0.2)
|
|
290
|
+
mixed = mix_colors(brightened, surface, 0.2)
|
|
291
|
+
else:
|
|
292
|
+
mixed = mix_colors(color, surface, 0.3)
|
|
293
|
+
else:
|
|
294
|
+
if lum > 0.5:
|
|
295
|
+
blended = mix_colors(color, surface, 0.2)
|
|
296
|
+
mixed = darken_color(blended, 0.15)
|
|
297
|
+
else:
|
|
298
|
+
brightened = lighten_color(color, 0.25)
|
|
299
|
+
mixed = mix_colors(brightened, surface, 0.25)
|
|
300
|
+
return mixed
|
|
301
|
+
|
|
302
|
+
def focus_inner(self, fill: str) -> str:
|
|
303
|
+
"""Internal focus line (2–3px), slightly brighter but not glowy."""
|
|
304
|
+
from bootstack.style.utility import contrast_ratio, hex_to_rgb
|
|
305
|
+
|
|
306
|
+
on = self.on_color(fill)
|
|
307
|
+
|
|
308
|
+
# Brighter defaults
|
|
309
|
+
w = 0.26 if self.provider.mode == "light" else 0.20
|
|
310
|
+
ring = mix_colors(on, fill, w)
|
|
311
|
+
|
|
312
|
+
# If still not distinct enough, bump once (bounded)
|
|
313
|
+
try:
|
|
314
|
+
bg = hex_to_rgb(fill)
|
|
315
|
+
fg = hex_to_rgb(ring)
|
|
316
|
+
if contrast_ratio(bg, fg) < 2.4:
|
|
317
|
+
w2 = min(w + 0.08, 0.34 if self.provider.mode == "light" else 0.28)
|
|
318
|
+
ring = mix_colors(on, fill, w2)
|
|
319
|
+
except Exception:
|
|
320
|
+
pass
|
|
321
|
+
|
|
322
|
+
return ring
|
|
323
|
+
|
|
324
|
+
def border(self, color: str, strength: float = 0.84) -> str:
|
|
325
|
+
"""Derive a stroke color for a given surface by blending toward the surface's
|
|
326
|
+
computed on-color (text/icon color).
|
|
327
|
+
"""
|
|
328
|
+
fg = self.on_color(color)
|
|
329
|
+
return mix_colors(color, fg, strength)
|
|
330
|
+
|
|
331
|
+
def on_color(self, color: str) -> str:
|
|
332
|
+
"""Return a readable foreground color for the given background.
|
|
333
|
+
|
|
334
|
+
This is intentionally biased so that *dark-ish* accents get light
|
|
335
|
+
text rather than the mathematically highest-contrast dark text,
|
|
336
|
+
which tends to look wrong on buttons and pills.
|
|
337
|
+
"""
|
|
338
|
+
background = self.color("background")
|
|
339
|
+
foreground = self.color("foreground")
|
|
340
|
+
|
|
341
|
+
try:
|
|
342
|
+
lum = relative_luminance(color)
|
|
343
|
+
except Exception:
|
|
344
|
+
candidates = [foreground, background, "#000000", "#ffffff"]
|
|
345
|
+
return best_foreground(color, candidates)
|
|
346
|
+
|
|
347
|
+
# Optional HSL-based accent detection to handle saturated mid-tone
|
|
348
|
+
# colors (like teal) that visually need light text even when their
|
|
349
|
+
# raw luminance is not very low.
|
|
350
|
+
hue = sat = hsl_lum = None
|
|
351
|
+
try:
|
|
352
|
+
hue, sat, hsl_lum = color_to_hsl(color, model="hex")
|
|
353
|
+
except Exception:
|
|
354
|
+
pass
|
|
355
|
+
|
|
356
|
+
if self.provider.mode == "light":
|
|
357
|
+
accent_force_light = False
|
|
358
|
+
if hue is not None and sat is not None and hsl_lum is not None:
|
|
359
|
+
# Treat saturated accents as needing light text when they are
|
|
360
|
+
# not extremely light overall. This catches teal/cyan/etc. but
|
|
361
|
+
# avoids triggering on near-white tinted backgrounds.
|
|
362
|
+
if sat >= 40 and hsl_lum <= 80:
|
|
363
|
+
# Strong teal/cyan/blue band (e.g., teal[600], cyan[600])
|
|
364
|
+
if 140 <= hue <= 220:
|
|
365
|
+
accent_force_light = True
|
|
366
|
+
# Other saturated accents, excluding the yellow/orange band,
|
|
367
|
+
# when not extremely light.
|
|
368
|
+
elif hsl_lum <= 70 and not (35 <= hue <= 70):
|
|
369
|
+
accent_force_light = True
|
|
370
|
+
|
|
371
|
+
# Saturated accents: force pure white text so contrast decisions
|
|
372
|
+
# don't accidentally favor dark foreground on mid-tone accents.
|
|
373
|
+
if accent_force_light:
|
|
374
|
+
candidates = ["#ffffff"]
|
|
375
|
+
# Anything darker than ~55% luminance is treated as a dark surface:
|
|
376
|
+
# always use light text, even if contrast math slightly prefers dark.
|
|
377
|
+
elif lum <= 0.55:
|
|
378
|
+
candidates = ["#ffffff"]
|
|
379
|
+
# Mid-light colors (e.g., warning/info) can work with either;
|
|
380
|
+
# allow contrast to choose, but still bias toward theme foreground.
|
|
381
|
+
elif lum <= 0.80:
|
|
382
|
+
candidates = [foreground, "#ffffff", "#000000"]
|
|
383
|
+
# Very light backgrounds -> dark text
|
|
384
|
+
else:
|
|
385
|
+
candidates = [foreground, "#000000"]
|
|
386
|
+
else:
|
|
387
|
+
# Dark mode: inverse bias.
|
|
388
|
+
if lum >= 0.75:
|
|
389
|
+
# Very light chips / badges -> dark text
|
|
390
|
+
candidates = ["#000000", foreground]
|
|
391
|
+
elif lum >= 0.45:
|
|
392
|
+
# Mid tones -> let contrast pick, but include light text
|
|
393
|
+
candidates = ["#ffffff", foreground, "#000000"]
|
|
394
|
+
else:
|
|
395
|
+
# Very dark surfaces -> light text
|
|
396
|
+
candidates = ["#ffffff", foreground]
|
|
397
|
+
|
|
398
|
+
# Deduplicate and remove empty candidates
|
|
399
|
+
unique: list[str] = []
|
|
400
|
+
for c in candidates:
|
|
401
|
+
if c and c not in unique:
|
|
402
|
+
unique.append(c)
|
|
403
|
+
|
|
404
|
+
return best_foreground(color, unique)
|
|
405
|
+
|
|
406
|
+
def muted_foreground(self, background: str, min_contrast: float = 4.5) -> str:
|
|
407
|
+
"""Return a muted foreground color with adequate contrast.
|
|
408
|
+
|
|
409
|
+
Generates a subdued text color that maintains readability across
|
|
410
|
+
varying backgrounds by ensuring minimum WCAG contrast requirements.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
background: The background color to contrast against.
|
|
414
|
+
min_contrast: Minimum WCAG contrast ratio (4.5 for AA, 7.0 for AAA).
|
|
415
|
+
Default is 4.5 (AA standard for normal text).
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
A muted foreground color with adequate contrast against the background.
|
|
419
|
+
"""
|
|
420
|
+
from bootstack.style.utility import hex_to_rgb, contrast_ratio
|
|
421
|
+
|
|
422
|
+
lum = relative_luminance(background)
|
|
423
|
+
bg_rgb = hex_to_rgb(background)
|
|
424
|
+
|
|
425
|
+
# Determine if we need light or dark muted text
|
|
426
|
+
if lum > 0.5:
|
|
427
|
+
# Light background -> use muted dark text
|
|
428
|
+
base_color = "#495057" # Dark gray
|
|
429
|
+
else:
|
|
430
|
+
# Dark background -> use muted light text
|
|
431
|
+
base_color = "#adb5bd" # Light gray
|
|
432
|
+
|
|
433
|
+
# Check if base muted color has adequate contrast
|
|
434
|
+
fg_rgb = hex_to_rgb(base_color)
|
|
435
|
+
ratio = contrast_ratio(bg_rgb, fg_rgb)
|
|
436
|
+
|
|
437
|
+
if ratio >= min_contrast:
|
|
438
|
+
return base_color
|
|
439
|
+
|
|
440
|
+
# If not enough contrast, adjust toward pure black/white
|
|
441
|
+
target = "#000000" if lum > 0.5 else "#ffffff"
|
|
442
|
+
|
|
443
|
+
# Binary search for minimum adjustment needed
|
|
444
|
+
for weight in [0.2, 0.4, 0.6, 0.8, 1.0]:
|
|
445
|
+
adjusted = mix_colors(target, base_color, weight)
|
|
446
|
+
adjusted_rgb = hex_to_rgb(adjusted)
|
|
447
|
+
ratio = contrast_ratio(bg_rgb, adjusted_rgb)
|
|
448
|
+
if ratio >= min_contrast:
|
|
449
|
+
return adjusted
|
|
450
|
+
|
|
451
|
+
# Fallback to pure contrast
|
|
452
|
+
return target
|
|
453
|
+
|
|
454
|
+
def disabled(self, role: str = "background", surface: str | None = None) -> str:
|
|
455
|
+
"""Return a disabled color mixed with the surface.
|
|
456
|
+
|
|
457
|
+
Args:
|
|
458
|
+
role: 'background' for surfaces, 'text' for foregrounds.
|
|
459
|
+
surface: Optional surface color to mix against. If omitted,
|
|
460
|
+
uses the theme background.
|
|
461
|
+
"""
|
|
462
|
+
surface = surface or self.color('background')
|
|
463
|
+
|
|
464
|
+
if role == "text":
|
|
465
|
+
if self.provider.mode == "light":
|
|
466
|
+
gray = "#6c757d"
|
|
467
|
+
mix_ratio = 0.35
|
|
468
|
+
else:
|
|
469
|
+
gray = "#adb5bd"
|
|
470
|
+
mix_ratio = 0.25
|
|
471
|
+
elif role == "background":
|
|
472
|
+
if self.provider.mode == "light":
|
|
473
|
+
gray = "#dee2e6"
|
|
474
|
+
mix_ratio = 0.15
|
|
475
|
+
else:
|
|
476
|
+
gray = "#495057"
|
|
477
|
+
mix_ratio = 0.20
|
|
478
|
+
else:
|
|
479
|
+
raise ValueError(f"Invalid role: {role}. Expected 'text' or 'background'.")
|
|
480
|
+
|
|
481
|
+
return mix_colors(gray, surface, mix_ratio)
|
|
482
|
+
|
|
483
|
+
def elevate(self, color: str, elevation: int = 0, max_elevation: int = 5) -> str:
|
|
484
|
+
if elevation <= 0:
|
|
485
|
+
return color
|
|
486
|
+
blend_target = "#000000" if self.provider.mode == "light" else "#ffffff"
|
|
487
|
+
weight = min(elevation / max_elevation, 1.0) * 0.3
|
|
488
|
+
return mix_colors(blend_target, color, weight)
|
|
489
|
+
|
|
490
|
+
@staticmethod
|
|
491
|
+
def _state_color(color: str, state: str) -> str:
|
|
492
|
+
if state == "focus":
|
|
493
|
+
return color
|
|
494
|
+
|
|
495
|
+
delta = {
|
|
496
|
+
"active": 0.08,
|
|
497
|
+
"selected": 0.18, # was effectively ~0.10–0.16; bump a bit
|
|
498
|
+
"pressed": 0.12,
|
|
499
|
+
"focus": 0.08,
|
|
500
|
+
}[state]
|
|
501
|
+
|
|
502
|
+
# Selected should read as "latched": always darken.
|
|
503
|
+
if state == "selected":
|
|
504
|
+
return darken_color(color, delta)
|
|
505
|
+
|
|
506
|
+
lum = relative_luminance(color)
|
|
507
|
+
if lum < 0.5:
|
|
508
|
+
return lighten_color(color, delta)
|
|
509
|
+
return darken_color(color, delta)
|
|
510
|
+
|
|
511
|
+
@staticmethod
|
|
512
|
+
def scale(value: Union[int, List, Tuple]):
|
|
513
|
+
return scale_size(value)
|
|
514
|
+
|
|
515
|
+
@staticmethod
|
|
516
|
+
def scale_from_source(value: Union[int, float, List, Tuple]):
|
|
517
|
+
"""Scale a value measured in source image pixels.
|
|
518
|
+
|
|
519
|
+
Source images are created at SOURCE_RESOLUTION (2x). This method
|
|
520
|
+
converts source pixel measurements to 1x values, then applies DPI scaling.
|
|
521
|
+
|
|
522
|
+
Use this when you measure border, padding, or other values directly
|
|
523
|
+
from your source image assets.
|
|
524
|
+
|
|
525
|
+
Args:
|
|
526
|
+
value: Pixel measurement from source image (int, float, or sequence)
|
|
527
|
+
|
|
528
|
+
Returns:
|
|
529
|
+
DPI-scaled value appropriate for the current display
|
|
530
|
+
|
|
531
|
+
Example:
|
|
532
|
+
# If your source image has 10px corners:
|
|
533
|
+
border=b.scale_from_source(10) # Correctly scales for all DPIs
|
|
534
|
+
"""
|
|
535
|
+
if isinstance(value, (int, float)):
|
|
536
|
+
return scale_size(int(value / SOURCE_RESOLUTION + 0.5))
|
|
537
|
+
elif isinstance(value, (tuple, list)):
|
|
538
|
+
scaled = [int(v / SOURCE_RESOLUTION + 0.5) for v in value]
|
|
539
|
+
return scale_size(scaled)
|
|
540
|
+
return scale_size(value)
|
|
541
|
+
|
|
542
|
+
# ----- Icon Utilities -----
|
|
543
|
+
|
|
544
|
+
@staticmethod
|
|
545
|
+
def normalize_icon_spec(icon: str | IconSpec, default_size: int = 18) -> IconSpec:
|
|
546
|
+
"""
|
|
547
|
+
If the icon is a string, then create a icon spec where the name is known and the size is set to default_size.
|
|
548
|
+
If the there is an icon spec, map the default size if one is not already specified in the spec.
|
|
549
|
+
|
|
550
|
+
Icon sizes are automatically scaled based on DPI settings. The default size of 20px provides
|
|
551
|
+
a balanced appearance, being slightly larger than the visible text (16px) but not overwhelming.
|
|
552
|
+
"""
|
|
553
|
+
from bootstack.runtime.utility import scale_size
|
|
554
|
+
|
|
555
|
+
# Apply DPI scaling to default size
|
|
556
|
+
scaled_default = scale_size(default_size)
|
|
557
|
+
|
|
558
|
+
if isinstance(icon, str):
|
|
559
|
+
return dict(name=icon, size=scaled_default)
|
|
560
|
+
else:
|
|
561
|
+
# Create a copy to avoid mutating the original dict
|
|
562
|
+
# This prevents size from growing on each theme change
|
|
563
|
+
result = icon.copy()
|
|
564
|
+
if 'size' not in result or result.get('size') is None:
|
|
565
|
+
result['size'] = scaled_default
|
|
566
|
+
elif result.get('size'):
|
|
567
|
+
# Scale user-provided size too
|
|
568
|
+
result['size'] = scale_size(result['size'])
|
|
569
|
+
return result
|
|
570
|
+
|
|
571
|
+
def map_stateful_icons(self, icon: IconSpec, foreground_spec: Sequence[tuple]):
|
|
572
|
+
"""
|
|
573
|
+
Build and return a TTK image state map for icons, using the
|
|
574
|
+
configured icon provider.
|
|
575
|
+
|
|
576
|
+
Parameters:
|
|
577
|
+
icon (IconSpec):
|
|
578
|
+
Base icon spec with default `name` and optional `size`/`color`.
|
|
579
|
+
Optional `state` provides per-state overrides where the value
|
|
580
|
+
can be either a string (state-specific icon name) or a dict
|
|
581
|
+
with `name` and/or `color`.
|
|
582
|
+
|
|
583
|
+
foreground_spec (Sequence[tuple]):
|
|
584
|
+
A sequence of state → foreground color items computed by the
|
|
585
|
+
builder, e.g.:
|
|
586
|
+
[
|
|
587
|
+
('disabled', '#fafafa'),
|
|
588
|
+
('pressed !disabled', '#63f3f3'),
|
|
589
|
+
('hover !disabled', '#f2fa5d'),
|
|
590
|
+
]
|
|
591
|
+
|
|
592
|
+
Behavior:
|
|
593
|
+
- If `icon.color` is provided at the root, use it as the default
|
|
594
|
+
color for all states unless a per-state override is provided.
|
|
595
|
+
- If `icon.color` is not provided, the icon color follows the
|
|
596
|
+
widget's foreground color provided by `foreground_spec`, unless
|
|
597
|
+
a per-state override is provided in `icon.state`.
|
|
598
|
+
- If a per-state override provides `name`, use that icon name for
|
|
599
|
+
the state; otherwise, fallback to the base icon `name`.
|
|
600
|
+
- Identical (name, size, color) combinations reuse the same image.
|
|
601
|
+
|
|
602
|
+
Returns:
|
|
603
|
+
list[tuple[str, str]]: List of (state, image_name) tuples suitable
|
|
604
|
+
for `ttk.Style.element_create(..., 'image', default, (state, image) ...)`.
|
|
605
|
+
"""
|
|
606
|
+
# Normalize base values
|
|
607
|
+
base_name: str = icon.get('name') # type: ignore[assignment]
|
|
608
|
+
if not base_name:
|
|
609
|
+
# Nothing we can do without an icon name
|
|
610
|
+
return []
|
|
611
|
+
|
|
612
|
+
base_size: int = int(icon.get('size') or 20)
|
|
613
|
+
base_color: str | None = icon.get('color')
|
|
614
|
+
|
|
615
|
+
# Build per-state override lookup: {state_str: {'name':..., 'color':...}}
|
|
616
|
+
state_overrides: dict[str, IconStateMap] = {}
|
|
617
|
+
for entry in icon.get('state', []) or []:
|
|
618
|
+
try:
|
|
619
|
+
st, ov = entry # type: ignore[misc]
|
|
620
|
+
except Exception:
|
|
621
|
+
continue
|
|
622
|
+
if isinstance(ov, str):
|
|
623
|
+
state_overrides[st] = {'name': ov}
|
|
624
|
+
elif isinstance(ov, dict):
|
|
625
|
+
# Only keep known keys
|
|
626
|
+
override: IconStateMap = {}
|
|
627
|
+
if 'name' in ov and ov['name']:
|
|
628
|
+
override['name'] = ov['name'] # type: ignore[assignment]
|
|
629
|
+
if 'color' in ov and ov['color']:
|
|
630
|
+
override['color'] = ov['color'] # type: ignore[assignment]
|
|
631
|
+
if override:
|
|
632
|
+
state_overrides[st] = override
|
|
633
|
+
|
|
634
|
+
# Obtain the provider callable (class or function). The provider itself
|
|
635
|
+
# is called as the icon constructor: provider(name, size, color) -> icon
|
|
636
|
+
|
|
637
|
+
def _resolve_fg(value: Any) -> str | None: # noqa: ANN401
|
|
638
|
+
"""Extract a color string from the foreground state spec value."""
|
|
639
|
+
if isinstance(value, str):
|
|
640
|
+
return value
|
|
641
|
+
if isinstance(value, dict):
|
|
642
|
+
# Try common keys in order
|
|
643
|
+
for k in ('foreground', 'text', 'color'):
|
|
644
|
+
v = value.get(k)
|
|
645
|
+
if isinstance(v, str):
|
|
646
|
+
return v
|
|
647
|
+
return None
|
|
648
|
+
|
|
649
|
+
# Cache icons by (name, size, color) to avoid duplicates
|
|
650
|
+
cache: dict[tuple[str, int, str | None], Any] = {}
|
|
651
|
+
|
|
652
|
+
def _image_for(name: str, size: int, color: str | None):
|
|
653
|
+
key = (name, size, color)
|
|
654
|
+
if key in cache:
|
|
655
|
+
return cache[key]
|
|
656
|
+
|
|
657
|
+
# Special case: 'empty' creates a transparent placeholder image
|
|
658
|
+
if name == 'empty':
|
|
659
|
+
from bootstack.style.utility import create_transparent_image
|
|
660
|
+
img = create_transparent_image(size, size)
|
|
661
|
+
cache[key] = img
|
|
662
|
+
return img
|
|
663
|
+
|
|
664
|
+
# Ensure our custom icon provider is initialized with the correct y_bias
|
|
665
|
+
_ensure_icon_provider()
|
|
666
|
+
|
|
667
|
+
# Call the provider directly; it returns an icon object with `.image`
|
|
668
|
+
try:
|
|
669
|
+
icon_obj = BootstrapIcon(name=name, size=size, color=color) # type: ignore[misc]
|
|
670
|
+
except TypeError:
|
|
671
|
+
icon_obj = BootstrapIcon(name, size, color) # type: ignore[misc]
|
|
672
|
+
if icon_obj is None:
|
|
673
|
+
return None
|
|
674
|
+
|
|
675
|
+
cache[key] = icon_obj.image
|
|
676
|
+
return icon_obj.image
|
|
677
|
+
|
|
678
|
+
state_image_specs: list[tuple[str, Any]] = []
|
|
679
|
+
|
|
680
|
+
def _match_override(expr: str) -> IconStateMap | None:
|
|
681
|
+
# 1) Exact expression match (e.g., 'hover !disabled')
|
|
682
|
+
if expr in state_overrides:
|
|
683
|
+
return state_overrides[expr]
|
|
684
|
+
# 2) Token match: treat state keywords as tokens; ignore negations like '!disabled'
|
|
685
|
+
tokens = {t for t in expr.split() if t and not t.startswith('!')}
|
|
686
|
+
for key, ov in state_overrides.items():
|
|
687
|
+
if not key:
|
|
688
|
+
# skip empty-key overrides here; apply only to '' base state
|
|
689
|
+
continue
|
|
690
|
+
if key in tokens:
|
|
691
|
+
return ov
|
|
692
|
+
# 3) No match
|
|
693
|
+
return None
|
|
694
|
+
|
|
695
|
+
# Build ordered list of states to map
|
|
696
|
+
base_states: list[str] = [s for s, _ in foreground_spec]
|
|
697
|
+
fg_lookup: dict[str, Any] = {s: v for s, v in foreground_spec}
|
|
698
|
+
|
|
699
|
+
# Ensure typical derivations for overrides exist (e.g., 'hover' -> 'hover !disabled')
|
|
700
|
+
def _derive_expr(k: str) -> str:
|
|
701
|
+
if not k:
|
|
702
|
+
return ''
|
|
703
|
+
if ' ' in k or '!' in k:
|
|
704
|
+
return k
|
|
705
|
+
if k in ('pressed', 'active'):
|
|
706
|
+
return 'pressed !disabled'
|
|
707
|
+
if k == 'selected':
|
|
708
|
+
return 'selected !disabled'
|
|
709
|
+
if k == 'hover':
|
|
710
|
+
return 'hover !disabled'
|
|
711
|
+
return k
|
|
712
|
+
|
|
713
|
+
extra_states: list[str] = []
|
|
714
|
+
seen = set(base_states)
|
|
715
|
+
default_present = '' in seen
|
|
716
|
+
if default_present:
|
|
717
|
+
# Exclude default for ordering; add back later
|
|
718
|
+
base_states_no_default = [s for s in base_states if s != '']
|
|
719
|
+
else:
|
|
720
|
+
base_states_no_default = base_states[:]
|
|
721
|
+
|
|
722
|
+
# Add override-derived states if not already present
|
|
723
|
+
for k in state_overrides.keys():
|
|
724
|
+
expr = _derive_expr(k)
|
|
725
|
+
if expr not in seen:
|
|
726
|
+
extra_states.append(expr)
|
|
727
|
+
seen.add(expr)
|
|
728
|
+
|
|
729
|
+
# Compose final order: base (without ''), then extras, then default ''
|
|
730
|
+
ordered_states = base_states_no_default + extra_states
|
|
731
|
+
if default_present:
|
|
732
|
+
ordered_states.append('')
|
|
733
|
+
|
|
734
|
+
for state_expr in ordered_states:
|
|
735
|
+
fg_val = fg_lookup.get(state_expr)
|
|
736
|
+
# Derive per-state name/color
|
|
737
|
+
override = _match_override(state_expr) or {}
|
|
738
|
+
name = override.get('name', base_name) # type: ignore[assignment]
|
|
739
|
+
# Determine color priority: per-state override > base_color > foreground state color
|
|
740
|
+
color = override.get('color') # type: ignore[assignment]
|
|
741
|
+
if color is None:
|
|
742
|
+
if base_color is not None:
|
|
743
|
+
color = base_color
|
|
744
|
+
else:
|
|
745
|
+
color = _resolve_fg(fg_val)
|
|
746
|
+
|
|
747
|
+
# Build or reuse the image
|
|
748
|
+
img_or_photo = _image_for(name, base_size, color)
|
|
749
|
+
if img_or_photo is not None:
|
|
750
|
+
state_image_specs.append((state_expr, img_or_photo))
|
|
751
|
+
|
|
752
|
+
return state_image_specs
|