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,696 @@
|
|
|
1
|
+
from colorsys import hls_to_rgb, rgb_to_hls
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Tuple, Union, cast
|
|
5
|
+
|
|
6
|
+
from PIL import Image, ImageChops, ImageColor, ImageOps, ImageDraw
|
|
7
|
+
from PIL.ImageTk import PhotoImage
|
|
8
|
+
|
|
9
|
+
from bootstack.core.images import Image as ImageService
|
|
10
|
+
from bootstack.runtime.utility import clamp
|
|
11
|
+
from bootstack.style.types import ColorModel
|
|
12
|
+
|
|
13
|
+
ASSETS_DIR = Path(__file__).parent.parent / "assets" / "widgets"
|
|
14
|
+
ELEMENTS_DIR = Path(__file__).parent.parent / "assets" / "elements"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class ElementMeta:
|
|
19
|
+
"""Scaled metadata for an element image.
|
|
20
|
+
|
|
21
|
+
All values are scaled for the current display DPI.
|
|
22
|
+
"""
|
|
23
|
+
width: int
|
|
24
|
+
height: int
|
|
25
|
+
border: Union[int, Tuple[int, ...]]
|
|
26
|
+
padding: Union[int, Tuple[int, ...]]
|
|
27
|
+
|
|
28
|
+
def border_spec(self) -> Union[int, Tuple[int, ...]]:
|
|
29
|
+
"""Return border in a format suitable for ttk element_create."""
|
|
30
|
+
return self.border
|
|
31
|
+
|
|
32
|
+
def padding_spec(self) -> Union[int, Tuple[int, ...]]:
|
|
33
|
+
"""Return padding in a format suitable for ttk element_create."""
|
|
34
|
+
return self.padding
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class ElementImageResult:
|
|
39
|
+
"""Result of recolor_element_image containing the image and its metadata."""
|
|
40
|
+
image: PhotoImage
|
|
41
|
+
meta: ElementMeta
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Cached manifest data
|
|
45
|
+
_manifest_cache: dict | None = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _load_manifest() -> dict:
|
|
49
|
+
"""Load and cache the element manifest."""
|
|
50
|
+
global _manifest_cache
|
|
51
|
+
if _manifest_cache is not None:
|
|
52
|
+
return _manifest_cache
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
import tomllib
|
|
56
|
+
except ImportError:
|
|
57
|
+
import tomli as tomllib # type: ignore[import-not-found]
|
|
58
|
+
|
|
59
|
+
manifest_path = ELEMENTS_DIR / "manifest.toml"
|
|
60
|
+
with open(manifest_path, "rb") as f:
|
|
61
|
+
_manifest_cache = tomllib.load(f)
|
|
62
|
+
|
|
63
|
+
return _manifest_cache
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _get_element_info(key: str) -> dict | None:
|
|
67
|
+
"""Get element info from manifest by key.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
key: The element key (e.g., 'button_md', 'checkbox_checked')
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Dict with file, width, height, border, padding or None if not found.
|
|
74
|
+
"""
|
|
75
|
+
manifest = _load_manifest()
|
|
76
|
+
images = manifest.get("images", {})
|
|
77
|
+
return images.get(key)
|
|
78
|
+
|
|
79
|
+
HUE = 360
|
|
80
|
+
SAT = 100
|
|
81
|
+
LUM = 100
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def create_box_image(width: int, height: int, color: str):
|
|
85
|
+
cache_key = f"{width}{height}{color}"
|
|
86
|
+
cached = ImageService.get_cached(cache_key)
|
|
87
|
+
if cached is not None:
|
|
88
|
+
return cached
|
|
89
|
+
|
|
90
|
+
img = Image.new("RGBA", (width, height), color)
|
|
91
|
+
pm = PhotoImage(image=img)
|
|
92
|
+
|
|
93
|
+
ImageService.set_cached(cache_key, pm)
|
|
94
|
+
return pm
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def create_rounded_border_image(*,
|
|
98
|
+
size=200,
|
|
99
|
+
radius=4,
|
|
100
|
+
thickness=2,
|
|
101
|
+
fill='#ffffff',
|
|
102
|
+
stroke='#000000'
|
|
103
|
+
):
|
|
104
|
+
cache_key = f"{size}{radius}{thickness}{fill}{stroke}"
|
|
105
|
+
cached = ImageService.get_cached(cache_key)
|
|
106
|
+
if cached is not None:
|
|
107
|
+
return cached
|
|
108
|
+
|
|
109
|
+
scale = 2 # light supersample for corner smoothing
|
|
110
|
+
|
|
111
|
+
s = size * scale
|
|
112
|
+
r = radius * scale
|
|
113
|
+
t = thickness * scale
|
|
114
|
+
|
|
115
|
+
img = Image.new("RGBA", (s, s), (0, 0, 0, 0))
|
|
116
|
+
d = ImageDraw.Draw(img)
|
|
117
|
+
|
|
118
|
+
# Two-rectangle approach for precise bounds control:
|
|
119
|
+
# 1. Outer rounded rect (stroke color) - fills to edges
|
|
120
|
+
# 2. Inner rounded rect (fill color) - inset by stroke thickness
|
|
121
|
+
outer_radius = r + t
|
|
122
|
+
d.rounded_rectangle(
|
|
123
|
+
(0, 0, s - 1, s - 1),
|
|
124
|
+
radius=outer_radius,
|
|
125
|
+
fill=stroke,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Inner rect inset by stroke thickness
|
|
129
|
+
d.rounded_rectangle(
|
|
130
|
+
(t, t, s - 1 - t, s - 1 - t),
|
|
131
|
+
radius=r,
|
|
132
|
+
fill=fill,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Resize with LANCZOS for smooth corners, then sharpen to restore edge crispness
|
|
136
|
+
from PIL import ImageFilter
|
|
137
|
+
img = img.resize((size, size), Image.Resampling.LANCZOS)
|
|
138
|
+
img = img.filter(ImageFilter.UnsharpMask(radius=0.5, percent=150, threshold=0))
|
|
139
|
+
|
|
140
|
+
pm = PhotoImage(image=img)
|
|
141
|
+
ImageService.set_cached(cache_key, pm)
|
|
142
|
+
return pm
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def create_transparent_image(width: int, height: int):
|
|
146
|
+
"""Create a transparent image."""
|
|
147
|
+
cache_key = f"transparent_{width}x{height}"
|
|
148
|
+
cached = ImageService.get_cached(cache_key)
|
|
149
|
+
if cached is not None:
|
|
150
|
+
return cached
|
|
151
|
+
|
|
152
|
+
img = Image.new('RGBA', (width, height), (255, 255, 255, 0))
|
|
153
|
+
pm = PhotoImage(image=img)
|
|
154
|
+
ImageService.set_cached(cache_key, pm)
|
|
155
|
+
return pm
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def color_to_rgb(color, model: ColorModel = 'hex'):
|
|
159
|
+
"""Convert color value to rgb.
|
|
160
|
+
|
|
161
|
+
The color and model parameters represent the color to be converted.
|
|
162
|
+
The value is expected to be a string for "name" and "hex" models and
|
|
163
|
+
a Tuple or List for "rgb" and "hsl" models.
|
|
164
|
+
|
|
165
|
+
**Parameters**
|
|
166
|
+
|
|
167
|
+
- `color` (Any): The color values for the model being converted.
|
|
168
|
+
- `model` (Literal['rbg', 'hsl', 'hex']): The color model being converted.
|
|
169
|
+
|
|
170
|
+
**Returns**
|
|
171
|
+
|
|
172
|
+
`Tuple[int, int, int]` — The rgb color values.
|
|
173
|
+
"""
|
|
174
|
+
conformed = conform_color_model(color, model)
|
|
175
|
+
return ImageColor.getrgb(conformed)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def color_to_hex(color, model: ColorModel = 'rgb'):
|
|
179
|
+
"""Convert color value to hex.
|
|
180
|
+
|
|
181
|
+
The color and model parameters represent the color to be converted.
|
|
182
|
+
The value is expected to be a string for "name" and "hex" models and
|
|
183
|
+
a Tuple or List for "rgb" and "hsl" models.
|
|
184
|
+
|
|
185
|
+
**Parameters**
|
|
186
|
+
|
|
187
|
+
- `color` (Any): The color values for the model being converted.
|
|
188
|
+
- `model` (Literal['rgb', 'hsl', 'hex']): The color model being converted.
|
|
189
|
+
|
|
190
|
+
**Returns**
|
|
191
|
+
|
|
192
|
+
`str` — The hexadecimal color value.
|
|
193
|
+
"""
|
|
194
|
+
r, g, b = color_to_rgb(color, model)
|
|
195
|
+
return f'#{r:02x}{g:02x}{b:02x}'
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def color_to_hsl(color, model: ColorModel = 'hex'):
|
|
199
|
+
"""Convert color value to hsl.
|
|
200
|
+
|
|
201
|
+
The color and model parameters represent the color to be converted.
|
|
202
|
+
The value is expected to be a string for "name" and "hex" models and
|
|
203
|
+
a Tuple or List for "rgb" and "hsl" models.
|
|
204
|
+
|
|
205
|
+
**Parameters**
|
|
206
|
+
|
|
207
|
+
- `color` (Any): The color values for the model being converted.
|
|
208
|
+
- `model` (Literal['rgb', 'hsl', 'hex']): The color model being converted.
|
|
209
|
+
|
|
210
|
+
**Returns**
|
|
211
|
+
|
|
212
|
+
`Tuple[int, int, int]` — The hsl color values.
|
|
213
|
+
"""
|
|
214
|
+
r, g, b = color_to_rgb(color, model)
|
|
215
|
+
hls = rgb_to_hls(r / 255, g / 255, b / 255)
|
|
216
|
+
hue = int(clamp(hls[0] * HUE, 0, HUE))
|
|
217
|
+
lum = int(clamp(hls[1] * LUM, 0, LUM))
|
|
218
|
+
sat = int(clamp(hls[2] * SAT, 0, SAT))
|
|
219
|
+
return hue, sat, lum
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def update_hsl_value(
|
|
223
|
+
color, hue=None, sat=None, lum=None,
|
|
224
|
+
in_model: ColorModel = 'hsl',
|
|
225
|
+
out_model: ColorModel = 'hsl'):
|
|
226
|
+
"""Change hue, saturation, or luminosity of the color based on the hue,
|
|
227
|
+
sat, lum parameters provided.
|
|
228
|
+
|
|
229
|
+
**Parameters**
|
|
230
|
+
|
|
231
|
+
- `color` (Any): The color.
|
|
232
|
+
- `hue` (int, optional): A number between 0 and 360.
|
|
233
|
+
- `sat` (int, optional): A number between 0 and 100.
|
|
234
|
+
- `lum` (int, optional): A number between 0 and 100.
|
|
235
|
+
- `in_model` (Literal['rgb', 'hsl', 'hex']): The color model of the input color.
|
|
236
|
+
- `out_model` (Literal['rgb', 'hsl', 'hex']): The color model of the output color.
|
|
237
|
+
|
|
238
|
+
**Returns**
|
|
239
|
+
|
|
240
|
+
`Tuple[int, int, int]` — The color value based on the selected color model.
|
|
241
|
+
"""
|
|
242
|
+
h, s, l = color_to_hsl(color, in_model)
|
|
243
|
+
if hue is not None:
|
|
244
|
+
h = hue
|
|
245
|
+
if sat is not None:
|
|
246
|
+
s = sat
|
|
247
|
+
if lum is not None:
|
|
248
|
+
l = lum
|
|
249
|
+
if out_model == 'rgb':
|
|
250
|
+
return color_to_rgb([h, s, l], 'hsl')
|
|
251
|
+
elif out_model == 'hex':
|
|
252
|
+
return color_to_hex([h, s, l], 'hsl')
|
|
253
|
+
else:
|
|
254
|
+
return h, s, l
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def contrast_color(
|
|
258
|
+
color, model: ColorModel, dark_color='#000',
|
|
259
|
+
light_color='#fff'):
|
|
260
|
+
"""The best matching contrasting light or dark color for the given color.
|
|
261
|
+
https://stackoverflow.com/questions/1855884/determine-font-color-based-on-background-color
|
|
262
|
+
|
|
263
|
+
**Parameters**
|
|
264
|
+
|
|
265
|
+
- `color` (str): The color value to evaluate.
|
|
266
|
+
- `model` (Literal['rgb', 'hsl', 'hex']): The model of the color value to be evaluated. 'rgb' by default.
|
|
267
|
+
- `dark_color` (str): The color of the dark contrasting color.
|
|
268
|
+
- `light_color` (str): The color of the light contrasting color.
|
|
269
|
+
|
|
270
|
+
**Returns**
|
|
271
|
+
|
|
272
|
+
`str` — The matching color value.
|
|
273
|
+
"""
|
|
274
|
+
if model != 'rgb':
|
|
275
|
+
r, g, b = color_to_rgb(color, model)
|
|
276
|
+
else:
|
|
277
|
+
r, g, b = color
|
|
278
|
+
|
|
279
|
+
luminance = ((0.299 * r) + (0.587 * g) + (0.114 * b)) / 255
|
|
280
|
+
if luminance > 0.5:
|
|
281
|
+
return dark_color
|
|
282
|
+
else:
|
|
283
|
+
return light_color
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def conform_color_model(color, model: ColorModel):
|
|
287
|
+
"""Conform the color values to a string that can be interpreted by the
|
|
288
|
+
`PIL.ImageColor.getrgb method`.
|
|
289
|
+
|
|
290
|
+
**Parameters**
|
|
291
|
+
|
|
292
|
+
- `color` (Any): The color value to conform.
|
|
293
|
+
- `model` (Literal['rgb', 'hsl', 'hex']): The model of the color to evaluate (rgb, hex, hsl).
|
|
294
|
+
|
|
295
|
+
**Returns**
|
|
296
|
+
|
|
297
|
+
`str` — A color value string that can be used as a parameter in the PIL.ImageColor.getrgb method.
|
|
298
|
+
"""
|
|
299
|
+
if model == 'hsl':
|
|
300
|
+
hue = clamp(color[0], 0, HUE)
|
|
301
|
+
sat = clamp(color[1], 0, SAT)
|
|
302
|
+
lum = clamp(color[2], 0, LUM)
|
|
303
|
+
return f'hsl({hue},{sat}%,{lum}%)'
|
|
304
|
+
elif model == 'rgb':
|
|
305
|
+
red = clamp(color[0], 0, 255)
|
|
306
|
+
grn = clamp(color[1], 0, 255)
|
|
307
|
+
blu = clamp(color[2], 0, 255)
|
|
308
|
+
return f'rgb({red},{grn},{blu})'
|
|
309
|
+
else:
|
|
310
|
+
return color
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def make_transparent(alpha, foreground, background='#fff'):
|
|
314
|
+
"""Simulate color transparency.
|
|
315
|
+
|
|
316
|
+
**Parameters**
|
|
317
|
+
|
|
318
|
+
- `alpha` (float): The amount of transparency between 0.0 and 1.0.
|
|
319
|
+
- `foreground` (str): The foreground color.
|
|
320
|
+
- `background` (str): The background color.
|
|
321
|
+
|
|
322
|
+
**Returns**
|
|
323
|
+
|
|
324
|
+
`str` — A hexadecimal color representing the "transparent" version of the foreground color against the background color.
|
|
325
|
+
"""
|
|
326
|
+
fg = ImageColor.getrgb(foreground)
|
|
327
|
+
bg = ImageColor.getrgb(background)
|
|
328
|
+
rgb_float = [alpha * c1 + (1 - alpha) * c2 for (c1, c2) in zip(fg, bg)]
|
|
329
|
+
rgb_int = [int(x) for x in rgb_float]
|
|
330
|
+
return '#{:02x}{:02x}{:02x}'.format(*rgb_int)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def open_image(name: str) -> Image.Image:
|
|
334
|
+
"""
|
|
335
|
+
Load a white-layout image from file.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
name: The asset name without extension (e.g. "add" loads "add.png")
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
A Pillow RGBA Image object.
|
|
342
|
+
"""
|
|
343
|
+
path = ASSETS_DIR / f"{name}.png"
|
|
344
|
+
return Image.open(path).convert("RGBA")
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _open_element_image(filename: str) -> Image.Image:
|
|
348
|
+
"""Load an element image from the elements directory.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
filename: The filename (e.g., "button-lg.png")
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
A Pillow RGBA Image object.
|
|
355
|
+
"""
|
|
356
|
+
path = ELEMENTS_DIR / filename
|
|
357
|
+
return Image.open(path).convert("RGBA")
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _scale_border_or_padding(
|
|
361
|
+
value: int | list | tuple,
|
|
362
|
+
scale: float
|
|
363
|
+
) -> int | tuple[int, ...]:
|
|
364
|
+
"""Scale border or padding values.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
value: Integer or sequence of integers from manifest
|
|
368
|
+
scale: Scale factor to apply
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
Scaled value(s) as int or tuple
|
|
372
|
+
"""
|
|
373
|
+
if isinstance(value, int):
|
|
374
|
+
return max(1, int(value * scale + 0.5)) if value > 0 else 0
|
|
375
|
+
elif isinstance(value, (list, tuple)):
|
|
376
|
+
scaled = tuple(
|
|
377
|
+
max(1, int(v * scale + 0.5)) if v > 0 else 0
|
|
378
|
+
for v in value
|
|
379
|
+
)
|
|
380
|
+
return scaled
|
|
381
|
+
return value
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def recolor_element_image(
|
|
385
|
+
key: str,
|
|
386
|
+
white_color: str,
|
|
387
|
+
black_color: str = "#ffffff",
|
|
388
|
+
magenta_color: str | None = None,
|
|
389
|
+
transparent_color: str | None = None,
|
|
390
|
+
) -> ElementImageResult:
|
|
391
|
+
"""Recolor an element image from the manifest and return scaled image with metadata.
|
|
392
|
+
|
|
393
|
+
This function fetches an image by key from the elements manifest, applies
|
|
394
|
+
luminance-based recoloring, and returns both the scaled image and scaled
|
|
395
|
+
metadata (border, padding, dimensions) for use with ttk element_create.
|
|
396
|
+
|
|
397
|
+
The scaling is cross-platform aware, handling the differences between
|
|
398
|
+
logical and physical pixels on macOS, Windows, and Linux:
|
|
399
|
+
- Windows: Uses system DPI scaling (96 DPI baseline, scales to 120, 144, etc.)
|
|
400
|
+
- macOS: Retina displays use 2x physical pixels per logical pixel
|
|
401
|
+
- Linux: Uses X11/Wayland DPI settings via winfo_fpixels
|
|
402
|
+
|
|
403
|
+
Source images are created at 2x resolution (default_dpi=2.0 in manifest),
|
|
404
|
+
so scaling accounts for this to produce correctly sized output.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
key: Element key from manifest (e.g., 'button_md', 'checkbox_checked')
|
|
408
|
+
white_color: Replace white/light areas with this color (hex string)
|
|
409
|
+
black_color: Replace black/dark areas with this color (hex string)
|
|
410
|
+
magenta_color: Replace magenta (#ff00ff) with this color, if provided
|
|
411
|
+
transparent_color: Fill fully transparent areas with this color
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
ElementImageResult containing:
|
|
415
|
+
- image: The recolored and scaled PhotoImage
|
|
416
|
+
- meta: ElementMeta with scaled width, height, border, and padding
|
|
417
|
+
|
|
418
|
+
Raises:
|
|
419
|
+
ValueError: If the key is not found in the manifest.
|
|
420
|
+
|
|
421
|
+
Example:
|
|
422
|
+
>>> result = recolor_element_image('button_md', '#3b82f6', '#1e40af')
|
|
423
|
+
>>> element = ElementImage(
|
|
424
|
+
... 'MyButton.border',
|
|
425
|
+
... result.image,
|
|
426
|
+
... border=result.meta.border,
|
|
427
|
+
... padding=result.meta.padding
|
|
428
|
+
... )
|
|
429
|
+
"""
|
|
430
|
+
# Get element info from manifest
|
|
431
|
+
info = _get_element_info(key)
|
|
432
|
+
if info is None:
|
|
433
|
+
raise ValueError(
|
|
434
|
+
f"Element key '{key}' not found in manifest. "
|
|
435
|
+
f"Check that the key exists in assets/elements/manifest.toml"
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
# Get source resolution from manifest
|
|
439
|
+
manifest = _load_manifest()
|
|
440
|
+
source_resolution = manifest.get("default_dpi", 2.0)
|
|
441
|
+
|
|
442
|
+
# Calculate cross-platform scale factor
|
|
443
|
+
from bootstack.runtime.utility import _ScalingState
|
|
444
|
+
scale = _ScalingState.get_image_scale(source_resolution=source_resolution)
|
|
445
|
+
|
|
446
|
+
# Create cache key from all parameters
|
|
447
|
+
cache_key = (
|
|
448
|
+
"element", key, white_color, black_color,
|
|
449
|
+
magenta_color, transparent_color, scale
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
# Check if we've already created this exact result
|
|
453
|
+
cached = ImageService.get_cached(cache_key)
|
|
454
|
+
if cached is not None:
|
|
455
|
+
# Cached value is a tuple of (PhotoImage, ElementMeta)
|
|
456
|
+
return ElementImageResult(image=cached[0], meta=cached[1])
|
|
457
|
+
|
|
458
|
+
# Load and process the image
|
|
459
|
+
filename = info.get("file", f"{key.replace('_', '-')}.png")
|
|
460
|
+
img = _open_element_image(filename)
|
|
461
|
+
gray = ImageOps.grayscale(img)
|
|
462
|
+
|
|
463
|
+
fg_rgb = color_to_rgb(white_color)
|
|
464
|
+
bg_rgb = color_to_rgb(black_color)
|
|
465
|
+
mag_rgb = color_to_rgb(magenta_color) if magenta_color else None
|
|
466
|
+
trans_rgb = color_to_rgb(transparent_color) if transparent_color else None
|
|
467
|
+
|
|
468
|
+
# Luminance-based recoloring via per-channel LUTs applied to grayscale.
|
|
469
|
+
# Output channel value = round(bg + (fg - bg) * gray / 255). PIL applies
|
|
470
|
+
# the LUT in C, replacing the per-pixel Python loop.
|
|
471
|
+
def _channel_lut(bg_c: int, fg_c: int) -> list[int]:
|
|
472
|
+
return [round(bg_c + (fg_c - bg_c) * i / 255.0) for i in range(256)]
|
|
473
|
+
|
|
474
|
+
r_chan = gray.point(_channel_lut(bg_rgb[0], fg_rgb[0]))
|
|
475
|
+
g_chan = gray.point(_channel_lut(bg_rgb[1], fg_rgb[1]))
|
|
476
|
+
b_chan = gray.point(_channel_lut(bg_rgb[2], fg_rgb[2]))
|
|
477
|
+
|
|
478
|
+
r_src, g_src, b_src, alpha = img.split()
|
|
479
|
+
result = Image.merge("RGBA", (r_chan, g_chan, b_chan, alpha))
|
|
480
|
+
|
|
481
|
+
# Magenta passthrough: pixels whose source RGB is exactly (255, 0, 255)
|
|
482
|
+
# are replaced with mag_rgb. Build a 0/255 mask via channel LUTs and
|
|
483
|
+
# AND them with ImageChops.multiply (255 * 255 / 255 = 255).
|
|
484
|
+
if mag_rgb:
|
|
485
|
+
r_eq = r_src.point([255 if i == 255 else 0 for i in range(256)])
|
|
486
|
+
g_eq = g_src.point([255 if i == 0 else 0 for i in range(256)])
|
|
487
|
+
b_eq = b_src.point([255 if i == 255 else 0 for i in range(256)])
|
|
488
|
+
mag_mask = ImageChops.multiply(ImageChops.multiply(r_eq, g_eq), b_eq)
|
|
489
|
+
mag_solid = Image.new("RGBA", img.size, (*mag_rgb, 255))
|
|
490
|
+
composited = Image.composite(mag_solid, result, mag_mask)
|
|
491
|
+
# composite replaces alpha too; restore the original alpha.
|
|
492
|
+
nr, ng, nb, _ = composited.split()
|
|
493
|
+
result = Image.merge("RGBA", (nr, ng, nb, alpha))
|
|
494
|
+
|
|
495
|
+
# Flatten partial transparency over a solid backing color. Matches the
|
|
496
|
+
# old per-pixel `r_final = trans*(1-a) + r*a, alpha=255` exactly.
|
|
497
|
+
if trans_rgb:
|
|
498
|
+
backing = Image.new("RGBA", img.size, (*trans_rgb, 255))
|
|
499
|
+
result = Image.alpha_composite(backing, result)
|
|
500
|
+
|
|
501
|
+
# Scale the image
|
|
502
|
+
# Use rounding (+ 0.5) to ensure image size matches metadata dimensions exactly.
|
|
503
|
+
# This prevents centering issues caused by mismatched image/border calculations.
|
|
504
|
+
if scale != 1.0:
|
|
505
|
+
new_size = (
|
|
506
|
+
max(1, int(result.width * scale + 0.5)),
|
|
507
|
+
max(1, int(result.height * scale + 0.5))
|
|
508
|
+
)
|
|
509
|
+
# Use NEAREST for sharp-edge assets (avoids antialiasing at color boundaries)
|
|
510
|
+
# Use LANCZOS for smooth assets (rounded corners, gradients)
|
|
511
|
+
resample_mode = info.get("resample", "lanczos").lower()
|
|
512
|
+
if resample_mode == "nearest":
|
|
513
|
+
result = result.resize(new_size, Image.Resampling.NEAREST)
|
|
514
|
+
else:
|
|
515
|
+
result = result.resize(new_size, Image.Resampling.LANCZOS)
|
|
516
|
+
|
|
517
|
+
photo_image = PhotoImage(image=result)
|
|
518
|
+
|
|
519
|
+
# Scale the metadata
|
|
520
|
+
source_width = info.get("width", img.width)
|
|
521
|
+
source_height = info.get("height", img.height)
|
|
522
|
+
source_border = info.get("border", 0)
|
|
523
|
+
source_padding = info.get("padding", 0)
|
|
524
|
+
|
|
525
|
+
meta = ElementMeta(
|
|
526
|
+
width=max(1, int(source_width * scale + 0.5)),
|
|
527
|
+
height=max(1, int(source_height * scale + 0.5)),
|
|
528
|
+
border=_scale_border_or_padding(source_border, scale),
|
|
529
|
+
padding=_scale_border_or_padding(source_padding, scale),
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
# Cache the result as a tuple
|
|
533
|
+
ImageService.set_cached(cache_key, (photo_image, meta))
|
|
534
|
+
|
|
535
|
+
return ElementImageResult(image=photo_image, meta=meta)
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def should_darken(bg_hex: str) -> bool:
|
|
539
|
+
"""Determine whether to darken or lighten based on luminance and saturation."""
|
|
540
|
+
r, g, b = [v / 255.0 for v in ImageColor.getrgb(bg_hex)]
|
|
541
|
+
h, l, s = rgb_to_hls(r, g, b)
|
|
542
|
+
|
|
543
|
+
# If the color is light and saturated, lighten it (like warning/info)
|
|
544
|
+
# If the color is already very light (lightness > 0.9), darken it to gain contrast
|
|
545
|
+
if l > 0.8:
|
|
546
|
+
return True # Darken very light colors (like `light`)
|
|
547
|
+
if l < 0.3:
|
|
548
|
+
return False # Lighten very dark colors (like `dark`)
|
|
549
|
+
if s > 0.6 and l > 0.6:
|
|
550
|
+
return False # Lighten vibrant, light colors (e.g. warning/info)
|
|
551
|
+
return True # Default: darken
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def darken_color(hex_color: str, percent: float) -> str:
|
|
555
|
+
"""Darken a hex color by reducing lightness in HLS color space."""
|
|
556
|
+
r, g, b = [v / 255.0 for v in ImageColor.getrgb(hex_color)]
|
|
557
|
+
h, l, s = rgb_to_hls(r, g, b)
|
|
558
|
+
l = max(0.0, l * (1 - percent))
|
|
559
|
+
r, g, b = hls_to_rgb(h, l, s)
|
|
560
|
+
return f"#{int(r * 255):02x}{int(g * 255):02x}{int(b * 255):02x}"
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def lighten_color(hex_color: str, percent: float) -> str:
|
|
564
|
+
"""Lighten a hex color by increasing lightness in HLS color space."""
|
|
565
|
+
r, g, b = [v / 255.0 for v in ImageColor.getrgb(hex_color)]
|
|
566
|
+
h, l, s = rgb_to_hls(r, g, b)
|
|
567
|
+
l = min(1.0, l + (1 - l) * percent)
|
|
568
|
+
r, g, b = hls_to_rgb(h, l, s)
|
|
569
|
+
return f"#{int(r * 255):02x}{int(g * 255):02x}{int(b * 255):02x}"
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def mix_colors(color1: str, color2: str, weight: float) -> str:
|
|
573
|
+
"""Mix two colors by weight.
|
|
574
|
+
|
|
575
|
+
Args:
|
|
576
|
+
color1: The foreground color in hex format (e.g., '#FF0000').
|
|
577
|
+
color2: The background color in hex format.
|
|
578
|
+
weight: A float from 0 to 1, where 1 favors color1 and 0 favors color2.
|
|
579
|
+
|
|
580
|
+
Returns:
|
|
581
|
+
A hex color string representing the mixed result.
|
|
582
|
+
"""
|
|
583
|
+
r1, g1, b1 = ImageColor.getrgb(color1)
|
|
584
|
+
r2, g2, b2 = ImageColor.getrgb(color2)
|
|
585
|
+
|
|
586
|
+
r = round(r1 * weight + r2 * (1 - weight))
|
|
587
|
+
g = round(g1 * weight + g2 * (1 - weight))
|
|
588
|
+
b = round(b1 * weight + b2 * (1 - weight))
|
|
589
|
+
|
|
590
|
+
return f"#{r:02X}{g:02X}{b:02X}"
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def tint_color(color: str, base_weight: float) -> str:
|
|
594
|
+
"""Tint a color by mixing it with white.
|
|
595
|
+
|
|
596
|
+
Args:
|
|
597
|
+
color: The base color in hex.
|
|
598
|
+
base_weight: Amount of base color to retain (0–1).
|
|
599
|
+
|
|
600
|
+
Returns:
|
|
601
|
+
A tinted hex color string.
|
|
602
|
+
"""
|
|
603
|
+
return mix_colors(color, "#ffffff", 1 - base_weight)
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def shade_color(color: str, base_weight: float) -> str:
|
|
607
|
+
"""Shade a color by mixing it with black.
|
|
608
|
+
|
|
609
|
+
Args:
|
|
610
|
+
color: The base color in hex.
|
|
611
|
+
base_weight: Amount of base color to retain (0–1).
|
|
612
|
+
|
|
613
|
+
Returns:
|
|
614
|
+
A shaded hex color string.
|
|
615
|
+
"""
|
|
616
|
+
return mix_colors(color, "#000000", 1 - base_weight)
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def relative_luminance(hex_color: str) -> float:
|
|
620
|
+
"""Calculate the relative luminance of a color.
|
|
621
|
+
|
|
622
|
+
Args:
|
|
623
|
+
hex_color: A hex color string.
|
|
624
|
+
|
|
625
|
+
Returns:
|
|
626
|
+
The luminance value from 0 (black) to 1 (white).
|
|
627
|
+
"""
|
|
628
|
+
r, g, b = [x / 255 for x in ImageColor.getrgb(hex_color)]
|
|
629
|
+
|
|
630
|
+
def adjust(c):
|
|
631
|
+
return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4
|
|
632
|
+
|
|
633
|
+
r, g, b = adjust(r), adjust(g), adjust(b)
|
|
634
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def contrast_ratio(rgb1: tuple[int, int, int], rgb2: tuple[int, int, int]) -> float:
|
|
638
|
+
def rel_luminance(r: int, g: int, b: int) -> float:
|
|
639
|
+
def channel(c): return (c / 255.0) ** 2.2
|
|
640
|
+
|
|
641
|
+
return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b)
|
|
642
|
+
|
|
643
|
+
lum1 = rel_luminance(*rgb1)
|
|
644
|
+
lum2 = rel_luminance(*rgb2)
|
|
645
|
+
lighter = max(lum1, lum2)
|
|
646
|
+
darker = min(lum1, lum2)
|
|
647
|
+
return (lighter + 0.05) / (darker + 0.05)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def hex_to_rgb(value: str) -> tuple[int, int, int]:
|
|
651
|
+
"""Convert a hex color string to an RGB tuple."""
|
|
652
|
+
value = value.lstrip("#")
|
|
653
|
+
lv = len(value)
|
|
654
|
+
result = tuple(int(value[i:i + lv // 3], 16) for i in range(0, lv, lv // 3))
|
|
655
|
+
return cast(tuple[int, int, int], result)
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def rgb_to_hex(rgb: tuple[int, int, int]) -> str:
|
|
659
|
+
"""Convert an RGB tuple to a hex color string."""
|
|
660
|
+
return "#{:02x}{:02x}{:02x}".format(*rgb)
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def hsl_to_hex(h: float, s: float, l: float) -> str:
|
|
664
|
+
"""Convert HSL values to a hex color string.
|
|
665
|
+
|
|
666
|
+
Args:
|
|
667
|
+
h: Hue in degrees (0-360)
|
|
668
|
+
s: Saturation as percentage (0-100)
|
|
669
|
+
l: Lightness as percentage (0-100)
|
|
670
|
+
|
|
671
|
+
Returns:
|
|
672
|
+
Hex color string (e.g., '#ff0000')
|
|
673
|
+
"""
|
|
674
|
+
# Normalize to 0-1 range
|
|
675
|
+
h_norm = h / 360.0
|
|
676
|
+
s_norm = s / 100.0
|
|
677
|
+
l_norm = l / 100.0
|
|
678
|
+
|
|
679
|
+
# colorsys uses HLS order (not HSL)
|
|
680
|
+
r, g, b = hls_to_rgb(h_norm, l_norm, s_norm)
|
|
681
|
+
|
|
682
|
+
return rgb_to_hex((round(r * 255), round(g * 255), round(b * 255)))
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def best_foreground(bg_color: str, candidates: list[str] = None) -> str:
|
|
686
|
+
"""Return the color with the highest contrast against the background."""
|
|
687
|
+
if candidates is None:
|
|
688
|
+
candidates = ["#000000", "#ffffff"]
|
|
689
|
+
|
|
690
|
+
bg_rgb = hex_to_rgb(bg_color)
|
|
691
|
+
|
|
692
|
+
def contrast(c: str) -> float:
|
|
693
|
+
fg_rgb = hex_to_rgb(c)
|
|
694
|
+
return contrast_ratio(bg_rgb, fg_rgb)
|
|
695
|
+
|
|
696
|
+
return max(candidates, key=contrast)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Theme definitions for bootstack.
|
|
2
|
+
|
|
3
|
+
This package contains theme definitions including standard built-in themes
|
|
4
|
+
and user-defined custom themes.
|
|
5
|
+
|
|
6
|
+
Modules:
|
|
7
|
+
standard: Built-in Bootstrap-inspired theme definitions
|
|
8
|
+
user: User-defined custom theme storage
|
|
9
|
+
|
|
10
|
+
The themes are defined as dictionaries containing color schemes and type
|
|
11
|
+
information (light/dark) used by the Style class to create themed widgets.
|
|
12
|
+
"""
|