bootstack 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bootstack/__init__.py +157 -0
- bootstack/__main__.py +5 -0
- bootstack/_core/__init__.py +21 -0
- bootstack/_core/capabilities/__init__.py +45 -0
- bootstack/_core/capabilities/after.py +103 -0
- bootstack/_core/capabilities/bind.py +192 -0
- bootstack/_core/capabilities/bindtags.py +112 -0
- bootstack/_core/capabilities/busy.py +72 -0
- bootstack/_core/capabilities/clipboard.py +89 -0
- bootstack/_core/capabilities/focus.py +118 -0
- bootstack/_core/capabilities/grab.py +65 -0
- bootstack/_core/capabilities/grid.py +211 -0
- bootstack/_core/capabilities/localization.py +231 -0
- bootstack/_core/capabilities/pack.py +140 -0
- bootstack/_core/capabilities/place.py +113 -0
- bootstack/_core/capabilities/selection.py +136 -0
- bootstack/_core/capabilities/signals.py +244 -0
- bootstack/_core/capabilities/winfo.py +315 -0
- bootstack/_core/colorutils.py +234 -0
- bootstack/_core/exceptions.py +25 -0
- bootstack/_core/images.py +463 -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/paths.py +49 -0
- bootstack/_core/publisher.py +149 -0
- bootstack/_core/variables.py +62 -0
- bootstack/_runtime/__init__.py +3 -0
- bootstack/_runtime/app.py +930 -0
- bootstack/_runtime/base_window.py +945 -0
- bootstack/_runtime/events.py +399 -0
- bootstack/_runtime/shortcuts.py +496 -0
- bootstack/_runtime/tk_patch.py +43 -0
- bootstack/_runtime/toplevel.py +276 -0
- bootstack/_runtime/utility.py +457 -0
- bootstack/_runtime/visual_focus.py +240 -0
- bootstack/_runtime/window_utilities.py +1123 -0
- bootstack/assets/__init__.py +21 -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/button-compact.png +0 -0
- bootstack/assets/elements/button-default.png +0 -0
- bootstack/assets/elements/buttongroup-after-h-compact.png +0 -0
- bootstack/assets/elements/buttongroup-after-h-default.png +0 -0
- bootstack/assets/elements/buttongroup-after-v-compact.png +0 -0
- bootstack/assets/elements/buttongroup-after-v-default.png +0 -0
- bootstack/assets/elements/buttongroup-before-h-compact.png +0 -0
- bootstack/assets/elements/buttongroup-before-h-default.png +0 -0
- bootstack/assets/elements/buttongroup-before-v-compact.png +0 -0
- bootstack/assets/elements/buttongroup-before-v-default.png +0 -0
- bootstack/assets/elements/buttongroup-center-h-compact.png +0 -0
- bootstack/assets/elements/buttongroup-center-h-default.png +0 -0
- bootstack/assets/elements/buttongroup-center-v-compact.png +0 -0
- bootstack/assets/elements/buttongroup-center-v-default.png +0 -0
- bootstack/assets/elements/card.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-addon-compact.png +0 -0
- bootstack/assets/elements/input-addon-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/listrow-compact.png +0 -0
- bootstack/assets/elements/listrow-default.png +0 -0
- bootstack/assets/elements/manifest.toml +361 -0
- bootstack/assets/elements/menu-item.png +0 -0
- bootstack/assets/elements/navitem-compact.png +0 -0
- bootstack/assets/elements/navitem-default.png +0 -0
- bootstack/assets/elements/progressbar-h-compact.png +0 -0
- bootstack/assets/elements/progressbar-h-default.png +0 -0
- bootstack/assets/elements/progressbar-v-compact.png +0 -0
- bootstack/assets/elements/progressbar-v-default.png +0 -0
- bootstack/assets/elements/radiobutton.png +0 -0
- bootstack/assets/elements/scrollbar-horizontal.png +0 -0
- bootstack/assets/elements/scrollbar-vertical.png +0 -0
- bootstack/assets/elements/slider-handle.png +0 -0
- bootstack/assets/elements/slider-track-h.png +0 -0
- bootstack/assets/elements/slider-track-v.png +0 -0
- bootstack/assets/elements/switch-off.png +0 -0
- bootstack/assets/elements/switch-on.png +0 -0
- bootstack/assets/elements/tab-h.png +0 -0
- bootstack/assets/elements/tab-v.png +0 -0
- bootstack/assets/icons/bootstrap.ttf +0 -0
- bootstack/assets/icons/glyphmap.json +2080 -0
- bootstack/assets/icons/icon_metrics.json +12470 -0
- bootstack/assets/locales/ar/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/ar/LC_MESSAGES/bootstack.po +856 -0
- bootstack/assets/locales/bg/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/bg/LC_MESSAGES/bootstack.po +878 -0
- bootstack/assets/locales/cs/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/cs/LC_MESSAGES/bootstack.po +856 -0
- bootstack/assets/locales/da/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/da/LC_MESSAGES/bootstack.po +856 -0
- bootstack/assets/locales/de/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/de/LC_MESSAGES/bootstack.po +856 -0
- bootstack/assets/locales/en/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/en/LC_MESSAGES/bootstack.po +878 -0
- bootstack/assets/locales/es/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/es/LC_MESSAGES/bootstack.po +856 -0
- bootstack/assets/locales/fr/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/fr/LC_MESSAGES/bootstack.po +856 -0
- bootstack/assets/locales/he/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/he/LC_MESSAGES/bootstack.po +854 -0
- bootstack/assets/locales/hi/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/hi/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/it/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/it/LC_MESSAGES/bootstack.po +844 -0
- bootstack/assets/locales/ja/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/ja/LC_MESSAGES/bootstack.po +917 -0
- bootstack/assets/locales/ko/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/ko/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/nb/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/nb/LC_MESSAGES/bootstack.po +844 -0
- bootstack/assets/locales/nl/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/nl/LC_MESSAGES/bootstack.po +844 -0
- bootstack/assets/locales/pl/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/pl/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/pt/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/pt/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/sl/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/sl/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/sv/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/sv/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/tr/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/tr/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.po +845 -0
- bootstack/cli/__init__.py +133 -0
- bootstack/cli/__main__.py +6 -0
- bootstack/cli/add.py +395 -0
- bootstack/cli/appicon.py +285 -0
- bootstack/cli/build.py +115 -0
- bootstack/cli/config.py +313 -0
- bootstack/cli/demo.py +564 -0
- bootstack/cli/dev.py +153 -0
- bootstack/cli/doctor.py +195 -0
- bootstack/cli/icons.py +98 -0
- bootstack/cli/promote.py +120 -0
- bootstack/cli/pyinstaller.py +268 -0
- bootstack/cli/run.py +95 -0
- bootstack/cli/start.py +117 -0
- bootstack/cli/templates/__init__.py +931 -0
- bootstack/clipboard.py +48 -0
- bootstack/constants.py +318 -0
- bootstack/data/README.md +615 -0
- bootstack/data/__init__.py +78 -0
- bootstack/data/_observable.py +276 -0
- bootstack/data/base.py +780 -0
- bootstack/data/file_source.py +367 -0
- bootstack/data/memory_source.py +450 -0
- bootstack/data/query.py +367 -0
- bootstack/data/readers.py +289 -0
- bootstack/data/sqlite_source.py +869 -0
- bootstack/data/types.py +354 -0
- bootstack/data/writers.py +232 -0
- bootstack/dev/__init__.py +36 -0
- bootstack/dev/_capture.py +141 -0
- bootstack/dev/_env.py +43 -0
- bootstack/dev/_registry.py +140 -0
- bootstack/dev/_reloader.py +351 -0
- bootstack/dev/_reset.py +50 -0
- bootstack/dev/_watcher.py +91 -0
- bootstack/dialogs/__init__.py +948 -0
- bootstack/dialogs/_impl/__init__.py +47 -0
- bootstack/dialogs/_impl/colorchooser.py +588 -0
- bootstack/dialogs/_impl/datedialog.py +450 -0
- bootstack/dialogs/_impl/dialog.py +594 -0
- bootstack/dialogs/_impl/filterdialog.py +358 -0
- bootstack/dialogs/_impl/fontdialog.py +364 -0
- bootstack/dialogs/_impl/formdialog.py +564 -0
- bootstack/dialogs/_impl/message.py +486 -0
- bootstack/dialogs/_impl/query.py +570 -0
- bootstack/errors.py +67 -0
- bootstack/events/__init__.py +111 -0
- bootstack/events/_event.py +135 -0
- bootstack/events/_payloads.py +539 -0
- bootstack/events/_subscription.py +38 -0
- bootstack/i18n/README.md +77 -0
- bootstack/i18n/__init__.py +23 -0
- bootstack/i18n/catalog.py +121 -0
- bootstack/i18n/intl_format.py +584 -0
- bootstack/i18n/msgcat.py +425 -0
- bootstack/i18n/specs.py +156 -0
- bootstack/images.py +563 -0
- bootstack/py.typed +1 -0
- bootstack/scheduling/__init__.py +11 -0
- bootstack/scheduling/_schedule.py +218 -0
- bootstack/shortcuts.py +21 -0
- bootstack/signals/README.md +98 -0
- bootstack/signals/__init__.py +10 -0
- bootstack/signals/integration.py +100 -0
- bootstack/signals/signal.py +353 -0
- bootstack/signals/types.py +6 -0
- bootstack/store.py +286 -0
- bootstack/streams/__init__.py +12 -0
- bootstack/streams/_stream.py +321 -0
- bootstack/style/__init__.py +38 -0
- bootstack/style/builders/__init__.py +51 -0
- bootstack/style/builders/badge.py +46 -0
- bootstack/style/builders/button.py +339 -0
- bootstack/style/builders/buttongroup.py +311 -0
- bootstack/style/builders/calendar.py +271 -0
- bootstack/style/builders/checkbutton.py +110 -0
- bootstack/style/builders/combobox.py +113 -0
- bootstack/style/builders/contextmenu.py +268 -0
- bootstack/style/builders/entry.py +82 -0
- bootstack/style/builders/expander.py +148 -0
- bootstack/style/builders/field.py +335 -0
- bootstack/style/builders/frame.py +50 -0
- bootstack/style/builders/label.py +28 -0
- bootstack/style/builders/labelframe.py +34 -0
- bootstack/style/builders/listview.py +369 -0
- bootstack/style/builders/menubar.py +91 -0
- bootstack/style/builders/menubutton.py +359 -0
- bootstack/style/builders/panedwindow.py +25 -0
- bootstack/style/builders/progressbar.py +67 -0
- bootstack/style/builders/radiobutton.py +99 -0
- bootstack/style/builders/scale.py +64 -0
- bootstack/style/builders/scrollbar.py +225 -0
- bootstack/style/builders/separator.py +49 -0
- bootstack/style/builders/sidenav.py +643 -0
- bootstack/style/builders/sizegrip.py +15 -0
- bootstack/style/builders/spinbox.py +119 -0
- bootstack/style/builders/switch.py +70 -0
- bootstack/style/builders/tabitem.py +204 -0
- bootstack/style/builders/togglegroup.py +294 -0
- bootstack/style/builders/toolbutton.py +275 -0
- bootstack/style/builders/tooltip.py +26 -0
- bootstack/style/builders/treeview.py +193 -0
- bootstack/style/builders/utils.py +455 -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/fonts.py +123 -0
- bootstack/style/style.py +609 -0
- bootstack/style/style_builder_base.py +716 -0
- bootstack/style/style_builder_mixed.py +93 -0
- bootstack/style/style_builder_tk.py +109 -0
- bootstack/style/style_builder_ttk.py +353 -0
- bootstack/style/style_resolver.py +447 -0
- bootstack/style/theme.py +245 -0
- bootstack/style/theme_provider.py +471 -0
- bootstack/style/themes/__init__.py +128 -0
- bootstack/style/tk_patch.py +5 -0
- bootstack/style/token_maps.py +41 -0
- bootstack/style/types.py +32 -0
- bootstack/style/typography.py +523 -0
- bootstack/style/utility.py +746 -0
- bootstack/types.py +39 -0
- bootstack/validation/__init__.py +6 -0
- bootstack/validation/types.py +15 -0
- bootstack/validation/validation_result.py +17 -0
- bootstack/validation/validation_rules.py +205 -0
- bootstack/widgets/__init__.py +74 -0
- bootstack/widgets/_core/__init__.py +31 -0
- bootstack/widgets/_core/app_config.py +266 -0
- bootstack/widgets/_core/base.py +461 -0
- bootstack/widgets/_core/container.py +334 -0
- bootstack/widgets/_core/context.py +35 -0
- bootstack/widgets/_core/events.py +130 -0
- bootstack/widgets/_core/field_mixin.py +353 -0
- bootstack/widgets/_core/icon_image_props.py +72 -0
- bootstack/widgets/_core/image_binding.py +93 -0
- bootstack/widgets/_core/navmodel.py +345 -0
- bootstack/widgets/_core/options.py +210 -0
- bootstack/widgets/_core/selection_group.py +130 -0
- bootstack/widgets/_core/window_controls.py +96 -0
- bootstack/widgets/_core/window_menu.py +246 -0
- bootstack/widgets/_impl/__init__.py +1 -0
- bootstack/widgets/_impl/_internal/__init__.py +0 -0
- bootstack/widgets/_impl/_internal/wrapper_base.py +307 -0
- bootstack/widgets/_impl/_parts/__init__.py +11 -0
- bootstack/widgets/_impl/_parts/numberentry_part.py +385 -0
- bootstack/widgets/_impl/_parts/spinnerentry_part.py +434 -0
- bootstack/widgets/_impl/_parts/textentry_part.py +406 -0
- bootstack/widgets/_impl/composites/__init__.py +33 -0
- bootstack/widgets/_impl/composites/_dateutils.py +33 -0
- bootstack/widgets/_impl/composites/_image_fit.py +105 -0
- bootstack/widgets/_impl/composites/accordion.py +381 -0
- bootstack/widgets/_impl/composites/avatar.py +191 -0
- bootstack/widgets/_impl/composites/buttongroup.py +371 -0
- bootstack/widgets/_impl/composites/calendar.py +972 -0
- bootstack/widgets/_impl/composites/carousel.py +567 -0
- bootstack/widgets/_impl/composites/chart.py +882 -0
- bootstack/widgets/_impl/composites/compositeframe.py +298 -0
- bootstack/widgets/_impl/composites/contextmenu.py +1951 -0
- bootstack/widgets/_impl/composites/dateentry.py +404 -0
- bootstack/widgets/_impl/composites/dropdownbutton.py +325 -0
- bootstack/widgets/_impl/composites/expander.py +515 -0
- bootstack/widgets/_impl/composites/field.py +670 -0
- bootstack/widgets/_impl/composites/form.py +1066 -0
- bootstack/widgets/_impl/composites/gallery.py +551 -0
- bootstack/widgets/_impl/composites/list/__init__.py +15 -0
- bootstack/widgets/_impl/composites/list/listitem.py +802 -0
- bootstack/widgets/_impl/composites/list/listview.py +1433 -0
- bootstack/widgets/_impl/composites/menu/__init__.py +18 -0
- bootstack/widgets/_impl/composites/menu/model.py +358 -0
- bootstack/widgets/_impl/composites/menu/render_native.py +134 -0
- bootstack/widgets/_impl/composites/menu/render_themed.py +134 -0
- bootstack/widgets/_impl/composites/meter.py +860 -0
- bootstack/widgets/_impl/composites/numericentry.py +201 -0
- bootstack/widgets/_impl/composites/pagestack.py +395 -0
- bootstack/widgets/_impl/composites/passwordentry.py +142 -0
- bootstack/widgets/_impl/composites/pathentry.py +168 -0
- bootstack/widgets/_impl/composites/picture.py +289 -0
- bootstack/widgets/_impl/composites/radiogroup.py +511 -0
- bootstack/widgets/_impl/composites/scrolledtext.py +375 -0
- bootstack/widgets/_impl/composites/scrolledtext.pyi +186 -0
- bootstack/widgets/_impl/composites/scrollview.py +764 -0
- bootstack/widgets/_impl/composites/selectbox.py +1026 -0
- bootstack/widgets/_impl/composites/shell/__init__.py +40 -0
- bootstack/widgets/_impl/composites/shell/content_host.py +52 -0
- bootstack/widgets/_impl/composites/shell/layout.py +335 -0
- bootstack/widgets/_impl/composites/shell/nav_panel.py +345 -0
- bootstack/widgets/_impl/composites/shell/providers.py +558 -0
- bootstack/widgets/_impl/composites/shell/rail.py +117 -0
- bootstack/widgets/_impl/composites/shell/shell.py +581 -0
- bootstack/widgets/_impl/composites/shell/workspace.py +273 -0
- bootstack/widgets/_impl/composites/sidenav/__init__.py +16 -0
- bootstack/widgets/_impl/composites/sidenav/header.py +81 -0
- bootstack/widgets/_impl/composites/sidenav/separator.py +44 -0
- bootstack/widgets/_impl/composites/slider/__init__.py +7 -0
- bootstack/widgets/_impl/composites/slider/_shared.py +195 -0
- bootstack/widgets/_impl/composites/slider/rangeslider.py +982 -0
- bootstack/widgets/_impl/composites/slider/slider.py +851 -0
- bootstack/widgets/_impl/composites/spinnerentry.py +185 -0
- bootstack/widgets/_impl/composites/tableview/__init__.py +5 -0
- bootstack/widgets/_impl/composites/tableview/tableview.py +3388 -0
- bootstack/widgets/_impl/composites/tableview/types.py +169 -0
- bootstack/widgets/_impl/composites/tabs/__init__.py +23 -0
- bootstack/widgets/_impl/composites/tabs/tabitem.py +389 -0
- bootstack/widgets/_impl/composites/tabs/tabs.py +974 -0
- bootstack/widgets/_impl/composites/tabs/tabview.py +650 -0
- bootstack/widgets/_impl/composites/textarea/__init__.py +26 -0
- bootstack/widgets/_impl/composites/textarea/change.py +46 -0
- bootstack/widgets/_impl/composites/textarea/codeeditor.py +526 -0
- bootstack/widgets/_impl/composites/textarea/core.py +495 -0
- bootstack/widgets/_impl/composites/textarea/decoration.py +42 -0
- bootstack/widgets/_impl/composites/textarea/diff.py +127 -0
- bootstack/widgets/_impl/composites/textarea/extensions/__init__.py +1 -0
- bootstack/widgets/_impl/composites/textarea/extensions/bracket_matcher.py +138 -0
- bootstack/widgets/_impl/composites/textarea/extensions/indent_guides.py +158 -0
- bootstack/widgets/_impl/composites/textarea/extensions/line_numbers.py +138 -0
- bootstack/widgets/_impl/composites/textarea/extensions/pygments_highlighter.py +312 -0
- bootstack/widgets/_impl/composites/textarea/extensions/smart_indent.py +177 -0
- bootstack/widgets/_impl/composites/textarea/filter.py +171 -0
- bootstack/widgets/_impl/composites/textarea/search_overlay.py +459 -0
- bootstack/widgets/_impl/composites/textarea/sidebar.py +88 -0
- bootstack/widgets/_impl/composites/textarea/style_registry.py +178 -0
- bootstack/widgets/_impl/composites/textarea/textarea.py +606 -0
- bootstack/widgets/_impl/composites/textarea/undo.py +217 -0
- bootstack/widgets/_impl/composites/textentry.py +57 -0
- bootstack/widgets/_impl/composites/timeentry.py +176 -0
- bootstack/widgets/_impl/composites/toast.py +390 -0
- bootstack/widgets/_impl/composites/toast_stack.py +156 -0
- bootstack/widgets/_impl/composites/togglegroup.py +418 -0
- bootstack/widgets/_impl/composites/toolbar.py +608 -0
- bootstack/widgets/_impl/composites/tooltip.py +491 -0
- bootstack/widgets/_impl/composites/tree/__init__.py +7 -0
- bootstack/widgets/_impl/composites/tree/source_binding.py +136 -0
- bootstack/widgets/_impl/composites/tree/treeitem.py +393 -0
- bootstack/widgets/_impl/composites/tree/treenode.py +174 -0
- bootstack/widgets/_impl/composites/tree/treeview.py +841 -0
- bootstack/widgets/_impl/mixins/__init__.py +24 -0
- bootstack/widgets/_impl/mixins/configure_mixin.py +216 -0
- bootstack/widgets/_impl/mixins/entry_mixin.py +134 -0
- bootstack/widgets/_impl/mixins/font_mixin.py +368 -0
- bootstack/widgets/_impl/mixins/icon_mixin.py +61 -0
- bootstack/widgets/_impl/mixins/localization_mixin.py +253 -0
- bootstack/widgets/_impl/mixins/signal_mixin.py +268 -0
- bootstack/widgets/_impl/mixins/validation_mixin.py +226 -0
- bootstack/widgets/_impl/primitives/__init__.py +49 -0
- bootstack/widgets/_impl/primitives/_menubutton.py +107 -0
- bootstack/widgets/_impl/primitives/badge.py +45 -0
- bootstack/widgets/_impl/primitives/button.py +76 -0
- bootstack/widgets/_impl/primitives/card.py +45 -0
- bootstack/widgets/_impl/primitives/checkbutton.py +124 -0
- bootstack/widgets/_impl/primitives/checktoggle.py +62 -0
- bootstack/widgets/_impl/primitives/combobox.py +156 -0
- bootstack/widgets/_impl/primitives/entry.py +87 -0
- bootstack/widgets/_impl/primitives/flexframe.py +448 -0
- bootstack/widgets/_impl/primitives/frame.py +185 -0
- bootstack/widgets/_impl/primitives/gridframe.py +546 -0
- bootstack/widgets/_impl/primitives/label.py +84 -0
- bootstack/widgets/_impl/primitives/labelframe.py +54 -0
- bootstack/widgets/_impl/primitives/optionmenu.py +387 -0
- bootstack/widgets/_impl/primitives/packframe.py +227 -0
- bootstack/widgets/_impl/primitives/panedwindow.py +45 -0
- bootstack/widgets/_impl/primitives/progressbar.py +83 -0
- bootstack/widgets/_impl/primitives/radiobutton.py +115 -0
- bootstack/widgets/_impl/primitives/radiotoggle.py +54 -0
- bootstack/widgets/_impl/primitives/scrollbar.py +42 -0
- bootstack/widgets/_impl/primitives/separator.py +43 -0
- bootstack/widgets/_impl/primitives/sizegrip.py +33 -0
- bootstack/widgets/_impl/primitives/spinbox.py +95 -0
- bootstack/widgets/_impl/primitives/switch.py +44 -0
- bootstack/widgets/_impl/primitives/treeview.py +69 -0
- bootstack/widgets/app.py +371 -0
- bootstack/widgets/appshell.py +1179 -0
- bootstack/widgets/avatar.py +140 -0
- bootstack/widgets/boolean_controls.py +455 -0
- bootstack/widgets/button.py +224 -0
- bootstack/widgets/buttongroup.py +239 -0
- bootstack/widgets/calendar.py +195 -0
- bootstack/widgets/card.py +159 -0
- bootstack/widgets/carousel.py +241 -0
- bootstack/widgets/chart.py +302 -0
- bootstack/widgets/codeeditor.py +675 -0
- bootstack/widgets/contextmenu.py +371 -0
- bootstack/widgets/datatable.py +688 -0
- bootstack/widgets/datefield.py +395 -0
- bootstack/widgets/divider.py +60 -0
- bootstack/widgets/expander.py +579 -0
- bootstack/widgets/form.py +200 -0
- bootstack/widgets/gallery.py +250 -0
- bootstack/widgets/gauge.py +168 -0
- bootstack/widgets/grid.py +121 -0
- bootstack/widgets/groupbox.py +162 -0
- bootstack/widgets/label.py +229 -0
- bootstack/widgets/listview.py +354 -0
- bootstack/widgets/menubutton.py +419 -0
- bootstack/widgets/numberfield.py +431 -0
- bootstack/widgets/pagestack.py +345 -0
- bootstack/widgets/passwordfield.py +383 -0
- bootstack/widgets/pathfield.py +454 -0
- bootstack/widgets/picture.py +251 -0
- bootstack/widgets/progressbar.py +106 -0
- bootstack/widgets/radio_variants.py +271 -0
- bootstack/widgets/radiogroup.py +228 -0
- bootstack/widgets/scrollbar.py +64 -0
- bootstack/widgets/scrollview.py +186 -0
- bootstack/widgets/select.py +302 -0
- bootstack/widgets/selectbutton.py +182 -0
- bootstack/widgets/sidebar_toggle.py +130 -0
- bootstack/widgets/sizegrip.py +42 -0
- bootstack/widgets/slider.py +408 -0
- bootstack/widgets/spinbox.py +147 -0
- bootstack/widgets/spinnerfield.py +413 -0
- bootstack/widgets/splash.py +367 -0
- bootstack/widgets/splitview.py +482 -0
- bootstack/widgets/stacks.py +249 -0
- bootstack/widgets/statusbar.py +189 -0
- bootstack/widgets/tabs.py +425 -0
- bootstack/widgets/textarea.py +459 -0
- bootstack/widgets/textfield.py +410 -0
- bootstack/widgets/theme_toggle.py +92 -0
- bootstack/widgets/timefield.py +340 -0
- bootstack/widgets/toast.py +314 -0
- bootstack/widgets/togglegroup.py +231 -0
- bootstack/widgets/toolbar.py +347 -0
- bootstack/widgets/tooltip.py +94 -0
- bootstack/widgets/tree.py +648 -0
- bootstack/widgets/types.py +305 -0
- bootstack/widgets/window.py +307 -0
- bootstack-0.1.0.dist-info/METADATA +301 -0
- bootstack-0.1.0.dist-info/RECORD +471 -0
- bootstack-0.1.0.dist-info/WHEEL +5 -0
- bootstack-0.1.0.dist-info/entry_points.txt +2 -0
- bootstack-0.1.0.dist-info/licenses/LICENSE +21 -0
- bootstack-0.1.0.dist-info/licenses/NOTICE +10 -0
- bootstack-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,716 @@
|
|
|
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 bootstack.style.theme_provider import ThemeProvider, use_theme
|
|
10
|
+
from bootstack.style.utility import best_foreground, color_to_hsl, darken_color, lighten_color, mix_colors, \
|
|
11
|
+
muted_foreground as _muted_foreground, relative_luminance
|
|
12
|
+
from bootstack._runtime.utility import scale_size
|
|
13
|
+
|
|
14
|
+
# Source images are created at this resolution multiplier (e.g., 2x)
|
|
15
|
+
# This allows measurements from source images to be used directly in code
|
|
16
|
+
SOURCE_RESOLUTION = 2.0
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class IconSpec(TypedDict, total=False):
|
|
20
|
+
name: str
|
|
21
|
+
size: Optional[int]
|
|
22
|
+
color: Optional[str]
|
|
23
|
+
state: Sequence[tuple[str, str | IconStateMap]]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class IconStateMap(TypedDict, total=False):
|
|
27
|
+
name: Optional[str]
|
|
28
|
+
color: Optional[str]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
ForegroundStateSpec = tuple[str, str | dict[str, str]]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class StyleBuilderBase:
|
|
35
|
+
"""Shared base for TTK and Tk style builders.
|
|
36
|
+
|
|
37
|
+
Centralizes theme provider plumbing and color utilities so both
|
|
38
|
+
StyleBuilder (TTK) and StyleBuilderTk (Tk) can inherit and
|
|
39
|
+
avoid duplication.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
_RE_TOKEN = re.compile(r"^\s*(?P<head>[a-zA-Z_][\w-]*)(?P<brackets>(?:\[[^]]*])*)\s*$")
|
|
43
|
+
|
|
44
|
+
def __init__(self, theme_provider: ThemeProvider | None = None, style_instance: Any | None = None): # noqa: ANN401
|
|
45
|
+
# If no provider given, try to derive from style_instance
|
|
46
|
+
if theme_provider is None and style_instance is not None:
|
|
47
|
+
try:
|
|
48
|
+
theme_provider = style_instance.theme_provider # type: ignore[attr-defined]
|
|
49
|
+
except Exception:
|
|
50
|
+
theme_provider = None
|
|
51
|
+
self._provider = theme_provider or use_theme()
|
|
52
|
+
self._style = style_instance
|
|
53
|
+
|
|
54
|
+
def set_style_instance(self, style_instance: Any) -> None: # noqa: ANN401
|
|
55
|
+
self._style = style_instance
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def provider(self) -> ThemeProvider:
|
|
59
|
+
return self._provider
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def style(self) -> Any | None: # noqa: ANN401
|
|
63
|
+
return self._style
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def colors(self) -> dict:
|
|
67
|
+
return self.provider.colors
|
|
68
|
+
|
|
69
|
+
# ----- Color Utilities & Transformers -----
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def default(value: Optional[str] = None):
|
|
73
|
+
if value == 'default':
|
|
74
|
+
return None
|
|
75
|
+
return value
|
|
76
|
+
|
|
77
|
+
def _parse_color_token(self, token: str):
|
|
78
|
+
"""Parse a color token into head and ordered list of modifiers.
|
|
79
|
+
|
|
80
|
+
Returns a dict with:
|
|
81
|
+
- head: The base color name (e.g., "primary")
|
|
82
|
+
- modifiers: List of (type, value) tuples in order
|
|
83
|
+
"""
|
|
84
|
+
m = self._RE_TOKEN.match(token)
|
|
85
|
+
if not m:
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
head = m.group("head")
|
|
89
|
+
brackets_raw = m.group("brackets")
|
|
90
|
+
|
|
91
|
+
modifiers = [] # List of (type, value) tuples in order
|
|
92
|
+
|
|
93
|
+
if brackets_raw:
|
|
94
|
+
# Extract all bracket contents: [content1][content2] -> ["content1", "content2"]
|
|
95
|
+
bracket_contents = re.findall(r'\[([^]]*)]', brackets_raw)
|
|
96
|
+
|
|
97
|
+
# Process each bracket in order for pipeline
|
|
98
|
+
for bracket in bracket_contents:
|
|
99
|
+
part = bracket.strip().lower()
|
|
100
|
+
if not part:
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
if part.isdigit():
|
|
104
|
+
modifiers.append(("shade", int(part)))
|
|
105
|
+
elif re.fullmatch(r'[+-]\s*\d+(?:\.\d+)?', part):
|
|
106
|
+
# Fractional steps allowed (e.g. "+0.85") for fine surface
|
|
107
|
+
# tints; elevate() blends continuously, so floats are exact.
|
|
108
|
+
rel = float(part.replace(" ", ""))
|
|
109
|
+
modifiers.append(("elevation", rel))
|
|
110
|
+
elif part == "subtle":
|
|
111
|
+
modifiers.append(("subtle", None))
|
|
112
|
+
elif part == "muted":
|
|
113
|
+
modifiers.append(("muted", None))
|
|
114
|
+
|
|
115
|
+
return {"head": head, "modifiers": modifiers}
|
|
116
|
+
|
|
117
|
+
def color(self, token: str, surface: str | None = None, role: str = "background") -> str:
|
|
118
|
+
"""Resolve a color token with optional chained modifiers.
|
|
119
|
+
|
|
120
|
+
Modifiers are applied as a pipeline from left to right:
|
|
121
|
+
- primary[100][muted] → lookup primary[100], then apply muted
|
|
122
|
+
- background[+1][muted] → lookup background, elevate it, then apply muted
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
token: Color token (e.g., "primary", "primary[100][muted]")
|
|
126
|
+
Can also be a hex color string (e.g., "#ff0000")
|
|
127
|
+
surface: Optional surface color for subtle modifier
|
|
128
|
+
role: Role for subtle modifier ("background" or "text")
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
The resolved color value as a hex string
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
ValueError: If the color token cannot be resolved to a valid color.
|
|
135
|
+
"""
|
|
136
|
+
# If token is already a hex color, return it directly
|
|
137
|
+
if token and token.startswith('#'):
|
|
138
|
+
return token
|
|
139
|
+
|
|
140
|
+
# Fast path: exact key (e.g., "blue[100]" or "primary")
|
|
141
|
+
direct = self.colors.get(token)
|
|
142
|
+
if direct is not None:
|
|
143
|
+
return direct
|
|
144
|
+
|
|
145
|
+
parsed = self._parse_color_token(token)
|
|
146
|
+
if not parsed:
|
|
147
|
+
result = self.colors.get(token)
|
|
148
|
+
if result is None:
|
|
149
|
+
raise ValueError(
|
|
150
|
+
f"Invalid color token: '{token}'. "
|
|
151
|
+
f"Valid color tokens include: primary, success, warning, danger, "
|
|
152
|
+
f"background, foreground, or a hex color like '#ff0000'."
|
|
153
|
+
)
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
head = parsed["head"]
|
|
157
|
+
modifiers = parsed["modifiers"]
|
|
158
|
+
|
|
159
|
+
# Determine base lookup key
|
|
160
|
+
# If first modifier is a shade, use it for lookup and remove from pipeline
|
|
161
|
+
base_key = head
|
|
162
|
+
if modifiers and modifiers[0][0] == "shade":
|
|
163
|
+
shade_value = modifiers[0][1]
|
|
164
|
+
base_key = f"{head}[{shade_value}]"
|
|
165
|
+
modifiers = modifiers[1:] # Remove shade from pipeline
|
|
166
|
+
|
|
167
|
+
# Get the base color
|
|
168
|
+
current_color = self.colors.get(base_key)
|
|
169
|
+
if current_color is None:
|
|
170
|
+
# Unknown base; fall back
|
|
171
|
+
result = self.colors.get(token)
|
|
172
|
+
if result is None:
|
|
173
|
+
raise ValueError(
|
|
174
|
+
f"Invalid color token: '{token}'. "
|
|
175
|
+
f"Valid color tokens include: primary, success, warning, danger, "
|
|
176
|
+
f"background, foreground, or a hex color like '#ff0000'."
|
|
177
|
+
)
|
|
178
|
+
return result
|
|
179
|
+
|
|
180
|
+
# Apply each modifier in order as a pipeline transformation
|
|
181
|
+
for mod_type, mod_value in modifiers:
|
|
182
|
+
if mod_type == "elevation":
|
|
183
|
+
current_color = self.elevate(current_color, mod_value)
|
|
184
|
+
elif mod_type == "subtle":
|
|
185
|
+
# Subtle needs special handling - it does its own lookup
|
|
186
|
+
# Use the base_key for lookup, not the current transformed color
|
|
187
|
+
current_color = self.subtle(base_key, surface, role)
|
|
188
|
+
elif mod_type == "muted":
|
|
189
|
+
# Muted transforms whatever color we currently have
|
|
190
|
+
current_color = self.muted_foreground(current_color)
|
|
191
|
+
|
|
192
|
+
return current_color
|
|
193
|
+
|
|
194
|
+
def subtle(self, token: str, surface: str | None = None, role: str = "background") -> str:
|
|
195
|
+
"""Return a subtle instance of this color for background or text."""
|
|
196
|
+
if token and token.startswith('#'):
|
|
197
|
+
base_color = token
|
|
198
|
+
else:
|
|
199
|
+
parsed = self._parse_color_token(token)
|
|
200
|
+
if parsed:
|
|
201
|
+
base_key = parsed["head"]
|
|
202
|
+
modifiers = parsed["modifiers"]
|
|
203
|
+
if modifiers and modifiers[0][0] == "shade":
|
|
204
|
+
base_key = f"{base_key}[{modifiers[0][1]}]"
|
|
205
|
+
base_color = self.colors.get(base_key)
|
|
206
|
+
else:
|
|
207
|
+
base_color = self.colors.get(token)
|
|
208
|
+
|
|
209
|
+
if base_color is None:
|
|
210
|
+
base_color = self.colors.get(token) or self.colors.get('foreground')
|
|
211
|
+
|
|
212
|
+
surface_val = surface or self.colors.get('background')
|
|
213
|
+
|
|
214
|
+
if role == "text":
|
|
215
|
+
# Prefer the precomputed per-semantic emphasis token (a readable,
|
|
216
|
+
# accent-tinted text color), else darken/lighten the base.
|
|
217
|
+
emphasis = self.colors.get(f"{token}_emphasis") if isinstance(token, str) else None
|
|
218
|
+
if emphasis:
|
|
219
|
+
return emphasis
|
|
220
|
+
if self.provider.mode == "light":
|
|
221
|
+
return darken_color(base_color, 0.25)
|
|
222
|
+
else:
|
|
223
|
+
return lighten_color(base_color, 0.25)
|
|
224
|
+
else: # background
|
|
225
|
+
# Blend the accent into the surface — surface-relative, so the wash
|
|
226
|
+
# reads in BOTH modes (a pure shade like [900] is surface-blind and
|
|
227
|
+
# lands darker than a dark surface, making the selection recede). Use
|
|
228
|
+
# the mode's accent stop: [500] (the bright anchor) on light, [400]
|
|
229
|
+
# (the dark-optimized, brighter accent) on dark so low-luminance hues
|
|
230
|
+
# like blue still lift off the dark surface. A gentler blend on light
|
|
231
|
+
# (the surface is bright, so a little tint reads); a touch more on
|
|
232
|
+
# dark where the wash must lift to register.
|
|
233
|
+
if self.provider.mode == "light":
|
|
234
|
+
stop, fraction = "500", 0.14
|
|
235
|
+
else:
|
|
236
|
+
stop, fraction = "400", 0.20
|
|
237
|
+
anchor = self.colors.get(f"{token}[{stop}]") if isinstance(token, str) else None
|
|
238
|
+
blend_into = anchor if anchor else base_color
|
|
239
|
+
return mix_colors(blend_into, surface_val, fraction)
|
|
240
|
+
|
|
241
|
+
def active(self, color: str) -> str:
|
|
242
|
+
return self._state_color(color, "active")
|
|
243
|
+
|
|
244
|
+
def pressed(self, color: str) -> str:
|
|
245
|
+
return self._state_color(color, "pressed")
|
|
246
|
+
|
|
247
|
+
def focus(self, color: str) -> str:
|
|
248
|
+
return self._state_color(color, "focus")
|
|
249
|
+
|
|
250
|
+
def selected(self, color: str) -> str:
|
|
251
|
+
return self._state_color(color, "selected")
|
|
252
|
+
|
|
253
|
+
def focus_border(self, color: str) -> str:
|
|
254
|
+
lum = relative_luminance(color)
|
|
255
|
+
if self.provider.mode == "dark":
|
|
256
|
+
return lighten_color(color, 0.1)
|
|
257
|
+
else:
|
|
258
|
+
return darken_color(color, 0.2 if lum > 0.5 else 0.1)
|
|
259
|
+
|
|
260
|
+
def focus_ring(self, color: str, surface: str | None = None) -> str:
|
|
261
|
+
surface = surface or self.color(color)
|
|
262
|
+
lum = relative_luminance(color)
|
|
263
|
+
if self.provider.mode == "dark":
|
|
264
|
+
if lum < 0.3:
|
|
265
|
+
brightened = lighten_color(color, 0.2)
|
|
266
|
+
mixed = mix_colors(brightened, surface, 0.2)
|
|
267
|
+
else:
|
|
268
|
+
mixed = mix_colors(color, surface, 0.3)
|
|
269
|
+
else:
|
|
270
|
+
if lum > 0.5:
|
|
271
|
+
blended = mix_colors(color, surface, 0.2)
|
|
272
|
+
mixed = darken_color(blended, 0.15)
|
|
273
|
+
else:
|
|
274
|
+
brightened = lighten_color(color, 0.25)
|
|
275
|
+
mixed = mix_colors(brightened, surface, 0.25)
|
|
276
|
+
return mixed
|
|
277
|
+
|
|
278
|
+
def focus_inner(self, fill: str) -> str:
|
|
279
|
+
"""Internal focus line (2–3px), slightly brighter but not glowy."""
|
|
280
|
+
from bootstack.style.utility import contrast_ratio, hex_to_rgb
|
|
281
|
+
|
|
282
|
+
on = self.on_color(fill)
|
|
283
|
+
|
|
284
|
+
# Brighter defaults
|
|
285
|
+
w = 0.26 if self.provider.mode == "light" else 0.20
|
|
286
|
+
ring = mix_colors(on, fill, w)
|
|
287
|
+
|
|
288
|
+
# If still not distinct enough, bump once (bounded)
|
|
289
|
+
try:
|
|
290
|
+
bg = hex_to_rgb(fill)
|
|
291
|
+
fg = hex_to_rgb(ring)
|
|
292
|
+
if contrast_ratio(bg, fg) < 2.4:
|
|
293
|
+
w2 = min(w + 0.08, 0.34 if self.provider.mode == "light" else 0.28)
|
|
294
|
+
ring = mix_colors(on, fill, w2)
|
|
295
|
+
except Exception:
|
|
296
|
+
pass
|
|
297
|
+
|
|
298
|
+
return ring
|
|
299
|
+
|
|
300
|
+
def border(self, color: str, strength: float = 0.84) -> str:
|
|
301
|
+
"""Derive a stroke color for a given surface by blending toward the surface's
|
|
302
|
+
computed on-color (text/icon color).
|
|
303
|
+
"""
|
|
304
|
+
fg = self.on_color(color)
|
|
305
|
+
return mix_colors(color, fg, strength)
|
|
306
|
+
|
|
307
|
+
def on_color(self, color: str) -> str:
|
|
308
|
+
"""Return a readable foreground color for the given background.
|
|
309
|
+
|
|
310
|
+
This is intentionally biased so that *dark-ish* accents get light
|
|
311
|
+
text rather than the mathematically highest-contrast dark text,
|
|
312
|
+
which tends to look wrong on buttons and pills.
|
|
313
|
+
"""
|
|
314
|
+
background = self.color("background")
|
|
315
|
+
foreground = self.color("foreground")
|
|
316
|
+
|
|
317
|
+
try:
|
|
318
|
+
lum = relative_luminance(color)
|
|
319
|
+
except Exception:
|
|
320
|
+
candidates = [foreground, background, "#000000", "#ffffff"]
|
|
321
|
+
return best_foreground(color, candidates)
|
|
322
|
+
|
|
323
|
+
# Optional HSL-based accent detection to handle saturated mid-tone
|
|
324
|
+
# colors (like teal) that visually need light text even when their
|
|
325
|
+
# raw luminance is not very low.
|
|
326
|
+
hue = sat = hsl_lum = None
|
|
327
|
+
try:
|
|
328
|
+
hue, sat, hsl_lum = color_to_hsl(color, model="hex")
|
|
329
|
+
except Exception:
|
|
330
|
+
pass
|
|
331
|
+
|
|
332
|
+
if self.provider.mode == "light":
|
|
333
|
+
accent_force_light = False
|
|
334
|
+
if hue is not None and sat is not None and hsl_lum is not None:
|
|
335
|
+
# Treat saturated accents as needing light text when they are
|
|
336
|
+
# not extremely light overall. This catches teal/cyan/etc. but
|
|
337
|
+
# avoids triggering on near-white tinted backgrounds.
|
|
338
|
+
if sat >= 40 and hsl_lum <= 80:
|
|
339
|
+
# Strong teal/cyan/blue band (e.g., teal[600], cyan[600])
|
|
340
|
+
if 140 <= hue <= 220:
|
|
341
|
+
accent_force_light = True
|
|
342
|
+
# Other saturated accents, excluding the yellow/orange band,
|
|
343
|
+
# when not extremely light.
|
|
344
|
+
elif hsl_lum <= 70 and not (35 <= hue <= 70):
|
|
345
|
+
accent_force_light = True
|
|
346
|
+
|
|
347
|
+
# Never force white when white is actually unreadable. A bright but
|
|
348
|
+
# mid-luminance accent like cyan (info) or yellow (warning) has far
|
|
349
|
+
# higher contrast with black than white, so it must take dark text —
|
|
350
|
+
# the luminance buckets alone would wrongly pick white. Gate the
|
|
351
|
+
# light-text bias on white clearing a real WCAG threshold.
|
|
352
|
+
#
|
|
353
|
+
# Use the large-text / UI threshold (3.0), not the normal-text 4.5:
|
|
354
|
+
# accent text is button/pill/badge labels (bold, >=14pt), so 3.0 is the
|
|
355
|
+
# correct WCAG bar. 4.5 was too strict and sent saturated mid-tone
|
|
356
|
+
# accents (e.g. dracula-light's purple primary, white ~3.7) to black,
|
|
357
|
+
# making them inconsistent with the same accent in dark mode (white).
|
|
358
|
+
contrast_white = 1.05 / (lum + 0.05) # ratio of #ffffff vs the color
|
|
359
|
+
white_ok = contrast_white >= 3.0
|
|
360
|
+
|
|
361
|
+
# Saturated / dark-ish accents prefer white text — but only when it
|
|
362
|
+
# is legible; otherwise fall through to a contrast-chosen dark color.
|
|
363
|
+
if (accent_force_light or lum <= 0.55) and white_ok:
|
|
364
|
+
candidates = ["#ffffff"]
|
|
365
|
+
# Mid-light colors (e.g., warning, info): let contrast choose, with a
|
|
366
|
+
# bias toward the theme foreground.
|
|
367
|
+
elif lum <= 0.80:
|
|
368
|
+
candidates = [foreground, "#000000", "#ffffff"]
|
|
369
|
+
# Very light backgrounds -> dark text
|
|
370
|
+
else:
|
|
371
|
+
candidates = [foreground, "#000000"]
|
|
372
|
+
else:
|
|
373
|
+
# Dark mode: inverse bias.
|
|
374
|
+
if lum >= 0.75:
|
|
375
|
+
# Very light chips / badges -> dark text
|
|
376
|
+
candidates = ["#000000", foreground]
|
|
377
|
+
elif lum >= 0.45:
|
|
378
|
+
# Mid tones -> let contrast pick, but include light text
|
|
379
|
+
candidates = ["#ffffff", foreground, "#000000"]
|
|
380
|
+
else:
|
|
381
|
+
# Very dark surfaces -> light text
|
|
382
|
+
candidates = ["#ffffff", foreground]
|
|
383
|
+
|
|
384
|
+
# Deduplicate and remove empty candidates
|
|
385
|
+
unique: list[str] = []
|
|
386
|
+
for c in candidates:
|
|
387
|
+
if c and c not in unique:
|
|
388
|
+
unique.append(c)
|
|
389
|
+
|
|
390
|
+
chosen = best_foreground(color, unique)
|
|
391
|
+
|
|
392
|
+
# Safety floor (3.0 = the large/bold WCAG bar that accent labels meet).
|
|
393
|
+
# An accent sitting near a luminance-bucket boundary can land on a biased
|
|
394
|
+
# candidate that is actually unreadable (e.g. a light amber `warning` just
|
|
395
|
+
# under the dark-mode 0.45 cutoff takes white at ~2:1). If the biased pick
|
|
396
|
+
# misses the bar, fall back to the higher-contrast of pure black/white.
|
|
397
|
+
def _ratio(fg: str) -> float:
|
|
398
|
+
try:
|
|
399
|
+
lf = relative_luminance(fg)
|
|
400
|
+
except Exception:
|
|
401
|
+
return 0.0
|
|
402
|
+
hi, lo = max(lf, lum), min(lf, lum)
|
|
403
|
+
return (hi + 0.05) / (lo + 0.05)
|
|
404
|
+
|
|
405
|
+
if _ratio(chosen) < 3.0:
|
|
406
|
+
chosen = "#000000" if _ratio("#000000") >= _ratio("#ffffff") else "#ffffff"
|
|
407
|
+
return chosen
|
|
408
|
+
|
|
409
|
+
def muted_foreground(self, background: str, min_contrast: float = 4.5) -> str:
|
|
410
|
+
"""Return a muted foreground color with adequate contrast against background."""
|
|
411
|
+
return _muted_foreground(background, min_contrast)
|
|
412
|
+
|
|
413
|
+
def disabled(self, role: str = "background", surface: str | None = None) -> str:
|
|
414
|
+
"""Return a disabled color mixed with the surface.
|
|
415
|
+
|
|
416
|
+
Args:
|
|
417
|
+
role: 'background' for surfaces, 'text' for foregrounds.
|
|
418
|
+
surface: Optional surface color to mix against. If omitted,
|
|
419
|
+
uses the theme background.
|
|
420
|
+
"""
|
|
421
|
+
surface = surface or self.color('background')
|
|
422
|
+
|
|
423
|
+
if role == "text":
|
|
424
|
+
if self.provider.mode == "light":
|
|
425
|
+
gray = "#6c757d"
|
|
426
|
+
mix_ratio = 0.35
|
|
427
|
+
else:
|
|
428
|
+
gray = "#adb5bd"
|
|
429
|
+
mix_ratio = 0.25
|
|
430
|
+
elif role == "background":
|
|
431
|
+
if self.provider.mode == "light":
|
|
432
|
+
gray = "#dee2e6"
|
|
433
|
+
mix_ratio = 0.15
|
|
434
|
+
else:
|
|
435
|
+
gray = "#495057"
|
|
436
|
+
mix_ratio = 0.20
|
|
437
|
+
else:
|
|
438
|
+
raise ValueError(f"Invalid role: {role}. Expected 'text' or 'background'.")
|
|
439
|
+
|
|
440
|
+
return mix_colors(gray, surface, mix_ratio)
|
|
441
|
+
|
|
442
|
+
def elevate(self, color: str, elevation: int = 0, max_elevation: int = 5) -> str:
|
|
443
|
+
if elevation <= 0:
|
|
444
|
+
return color
|
|
445
|
+
blend_target = "#000000" if self.provider.mode == "light" else "#ffffff"
|
|
446
|
+
weight = min(elevation / max_elevation, 1.0) * 0.3
|
|
447
|
+
return mix_colors(blend_target, color, weight)
|
|
448
|
+
|
|
449
|
+
@staticmethod
|
|
450
|
+
def _state_color(color: str, state: str) -> str:
|
|
451
|
+
if state == "focus":
|
|
452
|
+
return color
|
|
453
|
+
|
|
454
|
+
delta = {
|
|
455
|
+
"active": 0.08,
|
|
456
|
+
"selected": 0.18, # was effectively ~0.10–0.16; bump a bit
|
|
457
|
+
"pressed": 0.12,
|
|
458
|
+
"focus": 0.08,
|
|
459
|
+
}[state]
|
|
460
|
+
|
|
461
|
+
# Selected should read as "latched": always darken.
|
|
462
|
+
if state == "selected":
|
|
463
|
+
return darken_color(color, delta)
|
|
464
|
+
|
|
465
|
+
lum = relative_luminance(color)
|
|
466
|
+
if lum < 0.5:
|
|
467
|
+
return lighten_color(color, delta)
|
|
468
|
+
return darken_color(color, delta)
|
|
469
|
+
|
|
470
|
+
@staticmethod
|
|
471
|
+
def scale(value: Union[int, List, Tuple]):
|
|
472
|
+
return scale_size(value)
|
|
473
|
+
|
|
474
|
+
@staticmethod
|
|
475
|
+
def scale_from_source(value: Union[int, float, List, Tuple]):
|
|
476
|
+
"""Scale a value measured in source image pixels.
|
|
477
|
+
|
|
478
|
+
Source images are created at SOURCE_RESOLUTION (2x). This method
|
|
479
|
+
converts source pixel measurements to 1x values, then applies DPI scaling.
|
|
480
|
+
|
|
481
|
+
Use this when you measure border, padding, or other values directly
|
|
482
|
+
from your source image assets.
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
value: Pixel measurement from source image (int, float, or sequence)
|
|
486
|
+
|
|
487
|
+
Returns:
|
|
488
|
+
DPI-scaled value appropriate for the current display
|
|
489
|
+
|
|
490
|
+
Example:
|
|
491
|
+
# If your source image has 10px corners:
|
|
492
|
+
border=b.scale_from_source(10) # Correctly scales for all DPIs
|
|
493
|
+
"""
|
|
494
|
+
if isinstance(value, (int, float)):
|
|
495
|
+
return scale_size(int(value / SOURCE_RESOLUTION + 0.5))
|
|
496
|
+
elif isinstance(value, (tuple, list)):
|
|
497
|
+
scaled = [int(v / SOURCE_RESOLUTION + 0.5) for v in value]
|
|
498
|
+
return scale_size(scaled)
|
|
499
|
+
return scale_size(value)
|
|
500
|
+
|
|
501
|
+
# ----- Icon Utilities -----
|
|
502
|
+
|
|
503
|
+
@staticmethod
|
|
504
|
+
def normalize_icon_spec(icon: str | IconSpec, default_size: int = 18) -> IconSpec:
|
|
505
|
+
"""
|
|
506
|
+
If the icon is a string, then create a icon spec where the name is known and the size is set to default_size.
|
|
507
|
+
If the there is an icon spec, map the default size if one is not already specified in the spec.
|
|
508
|
+
|
|
509
|
+
Icon sizes are automatically scaled based on DPI settings. The default size of 20px provides
|
|
510
|
+
a balanced appearance, being slightly larger than the visible text (16px) but not overwhelming.
|
|
511
|
+
"""
|
|
512
|
+
from bootstack._runtime.utility import scale_size
|
|
513
|
+
|
|
514
|
+
# Apply DPI scaling to default size
|
|
515
|
+
scaled_default = scale_size(default_size)
|
|
516
|
+
|
|
517
|
+
if isinstance(icon, str):
|
|
518
|
+
return dict(name=icon, size=scaled_default)
|
|
519
|
+
else:
|
|
520
|
+
# Create a copy to avoid mutating the original dict
|
|
521
|
+
# This prevents size from growing on each theme change
|
|
522
|
+
result = icon.copy()
|
|
523
|
+
if 'size' not in result or result.get('size') is None:
|
|
524
|
+
result['size'] = scaled_default
|
|
525
|
+
elif result.get('size'):
|
|
526
|
+
# Scale user-provided size too
|
|
527
|
+
result['size'] = scale_size(result['size'])
|
|
528
|
+
return result
|
|
529
|
+
|
|
530
|
+
def _resolve_icon_color(self, color: Any) -> str | None:
|
|
531
|
+
"""Resolve an icon-spec color, turning a theme token into a hex value.
|
|
532
|
+
|
|
533
|
+
A hex value passes through; a theme token (such as `'primary'`) is
|
|
534
|
+
resolved against the active theme; any other string (e.g. a named color
|
|
535
|
+
like `'white'`) is left for the renderer to interpret.
|
|
536
|
+
"""
|
|
537
|
+
if not color:
|
|
538
|
+
return None
|
|
539
|
+
try:
|
|
540
|
+
return self.color(color)
|
|
541
|
+
except Exception:
|
|
542
|
+
return color
|
|
543
|
+
|
|
544
|
+
def map_stateful_icons(self, icon: IconSpec, foreground_spec: Sequence[tuple]):
|
|
545
|
+
"""
|
|
546
|
+
Build and return a TTK image state map for icons, using the
|
|
547
|
+
configured icon provider.
|
|
548
|
+
|
|
549
|
+
Parameters:
|
|
550
|
+
icon (IconSpec):
|
|
551
|
+
Base icon spec with default `name` and optional `size`/`color`.
|
|
552
|
+
Optional `state` provides per-state overrides where the value
|
|
553
|
+
can be either a string (state-specific icon name) or a dict
|
|
554
|
+
with `name` and/or `color`.
|
|
555
|
+
|
|
556
|
+
foreground_spec (Sequence[tuple]):
|
|
557
|
+
A sequence of state → foreground color items computed by the
|
|
558
|
+
builder, e.g.:
|
|
559
|
+
[
|
|
560
|
+
('disabled', '#fafafa'),
|
|
561
|
+
('pressed !disabled', '#63f3f3'),
|
|
562
|
+
('hover !disabled', '#f2fa5d'),
|
|
563
|
+
]
|
|
564
|
+
|
|
565
|
+
Behavior:
|
|
566
|
+
- If `icon.color` is provided at the root, use it as the default
|
|
567
|
+
color for all states unless a per-state override is provided.
|
|
568
|
+
- If `icon.color` is not provided, the icon color follows the
|
|
569
|
+
widget's foreground color provided by `foreground_spec`, unless
|
|
570
|
+
a per-state override is provided in `icon.state`.
|
|
571
|
+
- If a per-state override provides `name`, use that icon name for
|
|
572
|
+
the state; otherwise, fallback to the base icon `name`.
|
|
573
|
+
- Identical (name, size, color) combinations reuse the same image.
|
|
574
|
+
|
|
575
|
+
Returns:
|
|
576
|
+
list[tuple[str, str]]: List of (state, image_name) tuples suitable
|
|
577
|
+
for `ttk.Style.element_create(..., 'image', default, (state, image) ...)`.
|
|
578
|
+
"""
|
|
579
|
+
# Normalize base values
|
|
580
|
+
base_name: str = icon.get('name') # type: ignore[assignment]
|
|
581
|
+
if not base_name:
|
|
582
|
+
# Nothing we can do without an icon name
|
|
583
|
+
return []
|
|
584
|
+
|
|
585
|
+
base_size: int = int(icon.get('size') or 20)
|
|
586
|
+
base_color: str | None = self._resolve_icon_color(icon.get('color'))
|
|
587
|
+
|
|
588
|
+
# Build per-state override lookup: {state_str: {'name':..., 'color':...}}
|
|
589
|
+
state_overrides: dict[str, IconStateMap] = {}
|
|
590
|
+
for entry in icon.get('state', []) or []:
|
|
591
|
+
try:
|
|
592
|
+
st, ov = entry # type: ignore[misc]
|
|
593
|
+
except Exception:
|
|
594
|
+
continue
|
|
595
|
+
if isinstance(ov, str):
|
|
596
|
+
state_overrides[st] = {'name': ov}
|
|
597
|
+
elif isinstance(ov, dict):
|
|
598
|
+
# Only keep known keys
|
|
599
|
+
override: IconStateMap = {}
|
|
600
|
+
if 'name' in ov and ov['name']:
|
|
601
|
+
override['name'] = ov['name'] # type: ignore[assignment]
|
|
602
|
+
if 'color' in ov and ov['color']:
|
|
603
|
+
override['color'] = self._resolve_icon_color(ov['color']) # type: ignore[assignment]
|
|
604
|
+
if override:
|
|
605
|
+
state_overrides[st] = override
|
|
606
|
+
|
|
607
|
+
# Obtain the provider callable (class or function). The provider itself
|
|
608
|
+
# is called as the icon constructor: provider(name, size, color) -> icon
|
|
609
|
+
|
|
610
|
+
def _resolve_fg(value: Any) -> str | None: # noqa: ANN401
|
|
611
|
+
"""Extract a color string from the foreground state spec value."""
|
|
612
|
+
if isinstance(value, str):
|
|
613
|
+
return value
|
|
614
|
+
if isinstance(value, dict):
|
|
615
|
+
# Try common keys in order
|
|
616
|
+
for k in ('foreground', 'text', 'color'):
|
|
617
|
+
v = value.get(k)
|
|
618
|
+
if isinstance(v, str):
|
|
619
|
+
return v
|
|
620
|
+
return None
|
|
621
|
+
|
|
622
|
+
# Cache icons by (name, size, color) to avoid duplicates
|
|
623
|
+
cache: dict[tuple[str, int, str | None], Any] = {}
|
|
624
|
+
|
|
625
|
+
def _image_for(name: str, size: int, color: str | None):
|
|
626
|
+
key = (name, size, color)
|
|
627
|
+
if key in cache:
|
|
628
|
+
return cache[key]
|
|
629
|
+
|
|
630
|
+
# Special case: 'empty' creates a transparent placeholder image
|
|
631
|
+
if name == 'empty':
|
|
632
|
+
from bootstack.style.utility import create_transparent_image
|
|
633
|
+
img = create_transparent_image(size, size)
|
|
634
|
+
cache[key] = img
|
|
635
|
+
return img
|
|
636
|
+
|
|
637
|
+
from bootstack._core.images import _ImageService
|
|
638
|
+
img = _ImageService.get_icon(name, size, color or "black")
|
|
639
|
+
cache[key] = img
|
|
640
|
+
return img
|
|
641
|
+
|
|
642
|
+
state_image_specs: list[tuple[str, Any]] = []
|
|
643
|
+
|
|
644
|
+
def _match_override(expr: str) -> IconStateMap | None:
|
|
645
|
+
# 1) Exact expression match (e.g., 'hover !disabled')
|
|
646
|
+
if expr in state_overrides:
|
|
647
|
+
return state_overrides[expr]
|
|
648
|
+
# 2) Token match: treat state keywords as tokens; ignore negations like '!disabled'
|
|
649
|
+
tokens = {t for t in expr.split() if t and not t.startswith('!')}
|
|
650
|
+
for key, ov in state_overrides.items():
|
|
651
|
+
if not key:
|
|
652
|
+
# skip empty-key overrides here; apply only to '' base state
|
|
653
|
+
continue
|
|
654
|
+
if key in tokens:
|
|
655
|
+
return ov
|
|
656
|
+
# 3) No match
|
|
657
|
+
return None
|
|
658
|
+
|
|
659
|
+
# Build ordered list of states to map
|
|
660
|
+
base_states: list[str] = [s for s, _ in foreground_spec]
|
|
661
|
+
fg_lookup: dict[str, Any] = {s: v for s, v in foreground_spec}
|
|
662
|
+
|
|
663
|
+
# Ensure typical derivations for overrides exist (e.g., 'hover' -> 'hover !disabled')
|
|
664
|
+
def _derive_expr(k: str) -> str:
|
|
665
|
+
if not k:
|
|
666
|
+
return ''
|
|
667
|
+
if ' ' in k or '!' in k:
|
|
668
|
+
return k
|
|
669
|
+
if k in ('pressed', 'active'):
|
|
670
|
+
return 'pressed !disabled'
|
|
671
|
+
if k == 'selected':
|
|
672
|
+
return 'selected !disabled'
|
|
673
|
+
if k == 'hover':
|
|
674
|
+
return 'hover !disabled'
|
|
675
|
+
return k
|
|
676
|
+
|
|
677
|
+
extra_states: list[str] = []
|
|
678
|
+
seen = set(base_states)
|
|
679
|
+
default_present = '' in seen
|
|
680
|
+
if default_present:
|
|
681
|
+
# Exclude default for ordering; add back later
|
|
682
|
+
base_states_no_default = [s for s in base_states if s != '']
|
|
683
|
+
else:
|
|
684
|
+
base_states_no_default = base_states[:]
|
|
685
|
+
|
|
686
|
+
# Add override-derived states if not already present
|
|
687
|
+
for k in state_overrides.keys():
|
|
688
|
+
expr = _derive_expr(k)
|
|
689
|
+
if expr not in seen:
|
|
690
|
+
extra_states.append(expr)
|
|
691
|
+
seen.add(expr)
|
|
692
|
+
|
|
693
|
+
# Compose final order: base (without ''), then extras, then default ''
|
|
694
|
+
ordered_states = base_states_no_default + extra_states
|
|
695
|
+
if default_present:
|
|
696
|
+
ordered_states.append('')
|
|
697
|
+
|
|
698
|
+
for state_expr in ordered_states:
|
|
699
|
+
fg_val = fg_lookup.get(state_expr)
|
|
700
|
+
# Derive per-state name/color
|
|
701
|
+
override = _match_override(state_expr) or {}
|
|
702
|
+
name = override.get('name', base_name) # type: ignore[assignment]
|
|
703
|
+
# Determine color priority: per-state override > base_color > foreground state color
|
|
704
|
+
color = override.get('color') # type: ignore[assignment]
|
|
705
|
+
if color is None:
|
|
706
|
+
if base_color is not None:
|
|
707
|
+
color = base_color
|
|
708
|
+
else:
|
|
709
|
+
color = _resolve_fg(fg_val)
|
|
710
|
+
|
|
711
|
+
# Build or reuse the image
|
|
712
|
+
img_or_photo = _image_for(name, base_size, color)
|
|
713
|
+
if img_or_photo is not None:
|
|
714
|
+
state_image_specs.append((state_expr, img_or_photo))
|
|
715
|
+
|
|
716
|
+
return state_image_specs
|