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
bootstack/images.py
ADDED
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
"""Toolkit-free image handles and the icon factory.
|
|
2
|
+
|
|
3
|
+
This module is the public, framework-native way to work with images and icons:
|
|
4
|
+
|
|
5
|
+
- `Image` — a lightweight handle that holds image data (or a deferred icon spec)
|
|
6
|
+
and is only rendered for display when you hand it to a widget. You can build one
|
|
7
|
+
before an application exists; nothing about the underlying toolkit is exposed.
|
|
8
|
+
- `get_icon` — create a theme-aware `Image` from a Bootstrap Icons name. When the
|
|
9
|
+
color is given as a theme token, the icon re-renders automatically as the theme
|
|
10
|
+
changes.
|
|
11
|
+
- `AppIcon` — generate a platform application icon (a glyph on a filled,
|
|
12
|
+
rounded background) for an `App` or `Window`.
|
|
13
|
+
|
|
14
|
+
Examples:
|
|
15
|
+
```python
|
|
16
|
+
import bootstack as bs
|
|
17
|
+
from bootstack.images import Image, get_icon
|
|
18
|
+
|
|
19
|
+
with bs.App() as app:
|
|
20
|
+
bs.Label(image=Image.open("logo.png"))
|
|
21
|
+
bs.Button("Save", image=get_icon("save", color="primary"))
|
|
22
|
+
app.run()
|
|
23
|
+
```
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import atexit
|
|
29
|
+
import io
|
|
30
|
+
import math
|
|
31
|
+
import os
|
|
32
|
+
import re
|
|
33
|
+
import sys
|
|
34
|
+
import tempfile
|
|
35
|
+
from dataclasses import dataclass
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import TYPE_CHECKING
|
|
38
|
+
|
|
39
|
+
from bootstack._core.images import _ImageService
|
|
40
|
+
from bootstack._runtime.utility import scale_icon_size
|
|
41
|
+
|
|
42
|
+
if TYPE_CHECKING:
|
|
43
|
+
from PIL.Image import Image as _PILImage
|
|
44
|
+
from PIL.ImageTk import PhotoImage as _Photo
|
|
45
|
+
|
|
46
|
+
__all__ = ["Image", "get_icon", "AppIcon", "list_icons"]
|
|
47
|
+
|
|
48
|
+
# A color is treated as a literal hex value only when it starts with '#';
|
|
49
|
+
# every other string is a theme color token resolved against the active theme.
|
|
50
|
+
_HEX_RE = re.compile(r"^#[0-9a-fA-F]{3,8}$")
|
|
51
|
+
|
|
52
|
+
_DEFAULT_ICON_SIZE = 20
|
|
53
|
+
_DEFAULT_ICON_COLOR = "foreground"
|
|
54
|
+
|
|
55
|
+
# Sizes baked into a Windows multi-resolution `.ico`. Includes the DPI-scaled
|
|
56
|
+
# title-bar/taskbar sizes (20, 40, ...) so Windows finds an exact match at
|
|
57
|
+
# 125%/150%/200% scaling instead of upscaling the nearest one (which blurs).
|
|
58
|
+
_APP_ICON_SIZES = [16, 20, 24, 32, 40, 48, 64, 96, 128, 256]
|
|
59
|
+
|
|
60
|
+
# With shape='auto' on Windows/Linux, frames below this size are drawn glyph-only
|
|
61
|
+
# (a bare mark reads cleaner than a cramped tile in a tiny title bar) and frames
|
|
62
|
+
# at or above it get the rounded tile (a branded icon for the taskbar, the task
|
|
63
|
+
# switcher, and launchers).
|
|
64
|
+
_AUTO_TILE_MIN_SIZE = 24
|
|
65
|
+
# The rounded tile is supersampled to at least this canvas size before being
|
|
66
|
+
# downscaled, so its curved corners get antialiased (PIL draws them aliased at
|
|
67
|
+
# 1x). The glyph is NOT supersampled with it — it is rendered at its final size.
|
|
68
|
+
_MIN_TILE_CANVAS = 128
|
|
69
|
+
|
|
70
|
+
# Fraction of the icon edge the glyph box occupies, interpolated by size. Small
|
|
71
|
+
# icons use less padding (a bigger glyph) so the mark stays legible when there are
|
|
72
|
+
# few pixels; large icons get app-icon-style margins. (The glyph renderer adds a
|
|
73
|
+
# little inset of its own, so the visible mark is somewhat smaller than this.)
|
|
74
|
+
# Endpoints are reached at <= 16 px and >= 96 px; sizes between interpolate.
|
|
75
|
+
_GLYPH_FRAC_SMALL_SIZE = 16
|
|
76
|
+
_GLYPH_FRAC_LARGE_SIZE = 96
|
|
77
|
+
# On a tile the glyph sits inside the rounded background; glyph-only icons (no
|
|
78
|
+
# tile) let the mark nearly fill the frame.
|
|
79
|
+
_TILE_GLYPH_FRAC = (0.80, 0.64) # (small, large)
|
|
80
|
+
_GLYPH_ONLY_FRAC = (0.98, 0.90)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _glyph_frac(size: int, *, tiled: bool) -> float:
|
|
84
|
+
"""Glyph box fraction for `size`, smaller icons getting less padding."""
|
|
85
|
+
small, large = _TILE_GLYPH_FRAC if tiled else _GLYPH_ONLY_FRAC
|
|
86
|
+
lo, hi = _GLYPH_FRAC_SMALL_SIZE, _GLYPH_FRAC_LARGE_SIZE
|
|
87
|
+
if size <= lo:
|
|
88
|
+
return small
|
|
89
|
+
if size >= hi:
|
|
90
|
+
return large
|
|
91
|
+
t = (size - lo) / (hi - lo)
|
|
92
|
+
return small + t * (large - small)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _unlink_at_exit(path: str) -> None:
|
|
96
|
+
"""Best-effort removal of a generated runtime icon tempfile at interpreter exit."""
|
|
97
|
+
|
|
98
|
+
def _remove() -> None:
|
|
99
|
+
try:
|
|
100
|
+
os.unlink(path)
|
|
101
|
+
except OSError:
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
atexit.register(_remove)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _resolve_color(color: str) -> str:
|
|
108
|
+
"""Resolve a color string to a literal value.
|
|
109
|
+
|
|
110
|
+
A `#hex` value is returned unchanged; any other string is treated as a theme
|
|
111
|
+
color token and resolved against the active theme, falling back to the
|
|
112
|
+
literal string when there is no theme or the token is unknown.
|
|
113
|
+
"""
|
|
114
|
+
if _HEX_RE.match(color):
|
|
115
|
+
return color
|
|
116
|
+
from bootstack.style import get_theme_color
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
return get_theme_color(color)
|
|
120
|
+
except Exception:
|
|
121
|
+
return color
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass(frozen=True)
|
|
125
|
+
class _IconSpec:
|
|
126
|
+
"""A deferred icon: the parameters to render a glyph on demand."""
|
|
127
|
+
|
|
128
|
+
name: str
|
|
129
|
+
size: int
|
|
130
|
+
color: str
|
|
131
|
+
is_token: bool
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class Image:
|
|
135
|
+
"""A toolkit-free image handle, rendered for display only when used.
|
|
136
|
+
|
|
137
|
+
An `Image` carries a source — a file, raw bytes, an in-memory picture, or a
|
|
138
|
+
deferred icon — without creating any display resource up front. The picture
|
|
139
|
+
is rendered the moment it is handed to a widget (via `image=`), so a handle
|
|
140
|
+
can be built before an application is running and shared across widgets.
|
|
141
|
+
|
|
142
|
+
Build one with `Image.open`, `Image.from_bytes`, or `Image.from_pil`, or use
|
|
143
|
+
`get_icon` for a Bootstrap icon. Read `width` and `height` at any time.
|
|
144
|
+
|
|
145
|
+
File and byte sources accept the common raster image formats — PNG, JPEG,
|
|
146
|
+
GIF, BMP, TIFF, WebP, and ICO. (Animated formats load their first frame.)
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
__slots__ = ("_pil", "_path", "_data", "_icon", "_photo", "_icon_pil", "_size")
|
|
150
|
+
|
|
151
|
+
def __init__(
|
|
152
|
+
self,
|
|
153
|
+
*,
|
|
154
|
+
pil: "_PILImage | None" = None,
|
|
155
|
+
path: Path | None = None,
|
|
156
|
+
data: bytes | None = None,
|
|
157
|
+
icon: _IconSpec | None = None,
|
|
158
|
+
) -> None:
|
|
159
|
+
self._pil = pil
|
|
160
|
+
self._path = path
|
|
161
|
+
self._data = data
|
|
162
|
+
self._icon = icon
|
|
163
|
+
self._photo: "_Photo | None" = None
|
|
164
|
+
self._icon_pil: "_PILImage | None" = None # cached rendered-glyph PIL
|
|
165
|
+
self._size: tuple[int, int] | None = None
|
|
166
|
+
|
|
167
|
+
# =========================================================================
|
|
168
|
+
# Constructors
|
|
169
|
+
# =========================================================================
|
|
170
|
+
|
|
171
|
+
@classmethod
|
|
172
|
+
def open(cls, path: str | Path) -> "Image":
|
|
173
|
+
"""Create an image handle from a file on disk.
|
|
174
|
+
|
|
175
|
+
The file is decoded lazily, so the path is not read until the image is
|
|
176
|
+
displayed or its size is queried.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
path: Path to the image file. Supports `~` expansion. Accepts the
|
|
180
|
+
common raster formats — PNG, JPEG, GIF, BMP, TIFF, WebP, and ICO.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
A new image handle.
|
|
184
|
+
"""
|
|
185
|
+
return cls(path=Path(path).expanduser())
|
|
186
|
+
|
|
187
|
+
@classmethod
|
|
188
|
+
def from_bytes(cls, data: bytes) -> "Image":
|
|
189
|
+
"""Create an image handle from raw, encoded image bytes.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
data: Encoded image bytes in a common raster format — PNG, JPEG,
|
|
193
|
+
GIF, BMP, TIFF, WebP, or ICO — for example, an embedded resource
|
|
194
|
+
or a downloaded file.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
A new image handle.
|
|
198
|
+
"""
|
|
199
|
+
return cls(data=bytes(data))
|
|
200
|
+
|
|
201
|
+
@classmethod
|
|
202
|
+
def from_pil(cls, image: "_PILImage") -> "Image":
|
|
203
|
+
"""Create an image handle from an in-memory Pillow image.
|
|
204
|
+
|
|
205
|
+
Useful when you have already loaded or manipulated a picture with Pillow
|
|
206
|
+
(resizing, filtering, compositing) and want to display the result.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
image: A Pillow image object to wrap.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
A new image handle.
|
|
213
|
+
"""
|
|
214
|
+
return cls(pil=image)
|
|
215
|
+
|
|
216
|
+
# =========================================================================
|
|
217
|
+
# Size
|
|
218
|
+
# =========================================================================
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def width(self) -> int:
|
|
222
|
+
"""The width of the image in pixels."""
|
|
223
|
+
return self._dimensions()[0]
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def height(self) -> int:
|
|
227
|
+
"""The height of the image in pixels."""
|
|
228
|
+
return self._dimensions()[1]
|
|
229
|
+
|
|
230
|
+
def _dimensions(self) -> tuple[int, int]:
|
|
231
|
+
if self._size is None:
|
|
232
|
+
if self._icon is not None:
|
|
233
|
+
self._size = (self._icon.size, self._icon.size)
|
|
234
|
+
else:
|
|
235
|
+
pil = self._load_pil()
|
|
236
|
+
self._size = (pil.width, pil.height)
|
|
237
|
+
return self._size
|
|
238
|
+
|
|
239
|
+
def _load_pil(self) -> "_PILImage":
|
|
240
|
+
from PIL import Image as PILImage
|
|
241
|
+
|
|
242
|
+
if self._pil is not None:
|
|
243
|
+
return self._pil
|
|
244
|
+
if self._icon is not None:
|
|
245
|
+
# Render the glyph at the PIL level so icon handles work with the
|
|
246
|
+
# image-display widgets (Picture/Gallery) that fit/round/compose a
|
|
247
|
+
# PIL frame — not just the `icon=` PhotoImage path via _materialize.
|
|
248
|
+
# Cached so a recycling grid that re-resolves the same handle on
|
|
249
|
+
# every scroll doesn't re-render the glyph each time; dropped by
|
|
250
|
+
# _invalidate() on a theme change so token colors re-render.
|
|
251
|
+
if self._icon_pil is None:
|
|
252
|
+
size = scale_icon_size(self._icon.size)
|
|
253
|
+
size = size if size % 2 == 0 else size + 1
|
|
254
|
+
self._icon_pil = _ImageService._render_icon(
|
|
255
|
+
self._icon.name, size, self._resolve_icon_color()
|
|
256
|
+
)
|
|
257
|
+
return self._icon_pil
|
|
258
|
+
if self._path is not None:
|
|
259
|
+
return PILImage.open(self._path)
|
|
260
|
+
if self._data is not None:
|
|
261
|
+
return PILImage.open(io.BytesIO(self._data))
|
|
262
|
+
raise ValueError("image handle has no source")
|
|
263
|
+
|
|
264
|
+
# =========================================================================
|
|
265
|
+
# Internal rendering (called at the widget-binding boundary)
|
|
266
|
+
# =========================================================================
|
|
267
|
+
|
|
268
|
+
@property
|
|
269
|
+
def _is_theme_following(self) -> bool:
|
|
270
|
+
"""Whether this image must re-render when the theme changes."""
|
|
271
|
+
return self._icon is not None and self._icon.is_token
|
|
272
|
+
|
|
273
|
+
def _invalidate(self) -> None:
|
|
274
|
+
"""Drop the rendered image so the next bind re-renders it."""
|
|
275
|
+
self._photo = None
|
|
276
|
+
self._icon_pil = None
|
|
277
|
+
|
|
278
|
+
def _materialize(self, master=None) -> "_Photo":
|
|
279
|
+
"""Render the source to a cached display image and return it.
|
|
280
|
+
|
|
281
|
+
The `master` is accepted for interface symmetry; the backing service
|
|
282
|
+
renders against the default application root, which exists by the time a
|
|
283
|
+
handle is bound to a widget.
|
|
284
|
+
"""
|
|
285
|
+
if self._photo is not None:
|
|
286
|
+
return self._photo
|
|
287
|
+
if self._icon is not None:
|
|
288
|
+
color = self._resolve_icon_color()
|
|
289
|
+
photo = _ImageService.get_icon(self._icon.name, scale_icon_size(self._icon.size), color)
|
|
290
|
+
elif self._path is not None:
|
|
291
|
+
photo = _ImageService.open(self._path)
|
|
292
|
+
elif self._data is not None:
|
|
293
|
+
photo = _ImageService.from_bytes(self._data)
|
|
294
|
+
elif self._pil is not None:
|
|
295
|
+
photo = _ImageService.from_pil(self._pil)
|
|
296
|
+
else:
|
|
297
|
+
raise ValueError("image handle has no source")
|
|
298
|
+
self._photo = photo
|
|
299
|
+
return photo
|
|
300
|
+
|
|
301
|
+
def _resolve_icon_color(self) -> str:
|
|
302
|
+
spec = self._icon
|
|
303
|
+
assert spec is not None
|
|
304
|
+
if not spec.is_token:
|
|
305
|
+
return spec.color
|
|
306
|
+
return _resolve_color(spec.color)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def get_icon(
|
|
310
|
+
name: str,
|
|
311
|
+
*,
|
|
312
|
+
size: int = _DEFAULT_ICON_SIZE,
|
|
313
|
+
color: str = _DEFAULT_ICON_COLOR,
|
|
314
|
+
) -> Image:
|
|
315
|
+
"""Create a theme-aware icon image from a Bootstrap Icons name.
|
|
316
|
+
|
|
317
|
+
The returned handle can be passed to any widget that accepts `image=`. When
|
|
318
|
+
`color` is a theme color token (the default), the icon re-renders to match
|
|
319
|
+
whenever the theme changes; a literal hex color stays fixed.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
name: Bootstrap Icons name, for example `'house'` or `'arrow-right'`.
|
|
323
|
+
Browse the catalog at https://icons.getbootstrap.com.
|
|
324
|
+
size: Icon size in pixels. Defaults to `20`.
|
|
325
|
+
color: A theme color token (such as `'foreground'`, `'primary'`, or
|
|
326
|
+
`'danger'`) or a literal hex string like `'#ff8800'`. Token colors
|
|
327
|
+
follow the active theme; hex colors are fixed. Defaults to
|
|
328
|
+
`'foreground'`.
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
A new image handle that renders the icon on demand.
|
|
332
|
+
"""
|
|
333
|
+
is_hex = bool(_HEX_RE.match(color))
|
|
334
|
+
spec = _IconSpec(name=name, size=int(size), color=color, is_token=not is_hex)
|
|
335
|
+
return Image(icon=spec)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def list_icons(contains: str | None = None) -> list[str]:
|
|
339
|
+
"""List the available Bootstrap Icons names, sorted.
|
|
340
|
+
|
|
341
|
+
Each name can be used as a widget's `icon=` or passed to `get_icon`.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
contains: If given, only names containing this text (case-insensitive)
|
|
345
|
+
are returned. Defaults to `None` (all names).
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
A sorted list of icon names.
|
|
349
|
+
"""
|
|
350
|
+
_, glyphmap = _ImageService._load_icon_assets()
|
|
351
|
+
names = sorted(glyphmap)
|
|
352
|
+
if contains:
|
|
353
|
+
needle = contains.lower()
|
|
354
|
+
names = [n for n in names if needle in n]
|
|
355
|
+
return names
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
class AppIcon:
|
|
359
|
+
"""A generated application icon.
|
|
360
|
+
|
|
361
|
+
Renders a Bootstrap glyph in one of two shapes:
|
|
362
|
+
|
|
363
|
+
- a **tile** — the glyph in `foreground` on a filled, rounded `background`
|
|
364
|
+
(the macOS look); or
|
|
365
|
+
- **glyph-only** — the glyph alone in the `background` (brand) color on a
|
|
366
|
+
transparent field (the typical Windows and Linux look).
|
|
367
|
+
|
|
368
|
+
By default (`shape='auto'`) the shape follows the platform and the size: a
|
|
369
|
+
tile for macOS targets (`.icns`, or running on macOS); on Windows and Linux,
|
|
370
|
+
glyph-only at small sizes (a clean mark in a tiny title bar) and a tile at
|
|
371
|
+
larger sizes (a branded icon in the taskbar and launchers). A multi-size
|
|
372
|
+
`.ico` therefore carries both. Set `shape` to `'tile'` or `'glyph'` to force
|
|
373
|
+
one shape at every size.
|
|
374
|
+
|
|
375
|
+
Pass an `AppIcon` as the `icon=` of an `App` or `Window` to set its taskbar
|
|
376
|
+
and title-bar icon without supplying an icon file, or call `save` to export a
|
|
377
|
+
packaging asset (for example, an icon for a PyInstaller build).
|
|
378
|
+
|
|
379
|
+
Colors may be theme color tokens (such as `'primary'`) or literal hex
|
|
380
|
+
strings. Tokens are resolved once, when the icon is generated, so the icon
|
|
381
|
+
does not change as the theme changes; supply a token while an application is
|
|
382
|
+
active, or use hex values. Generating an icon outside a running application —
|
|
383
|
+
such as during a build — must use hex values, since no theme is available to
|
|
384
|
+
resolve a token against.
|
|
385
|
+
"""
|
|
386
|
+
|
|
387
|
+
def __init__(
|
|
388
|
+
self,
|
|
389
|
+
icon: str,
|
|
390
|
+
*,
|
|
391
|
+
background: str = "primary",
|
|
392
|
+
foreground: str = "white",
|
|
393
|
+
radius: float = 0.22,
|
|
394
|
+
shape: str = "auto",
|
|
395
|
+
) -> None:
|
|
396
|
+
"""Describe an application icon.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
icon: Bootstrap Icons name to draw, for example `'rocket'`.
|
|
400
|
+
background: Brand color — fills the rounded background of a tile, or
|
|
401
|
+
colors the glyph itself when glyph-only. A theme color token or a
|
|
402
|
+
hex string. Defaults to `'primary'`.
|
|
403
|
+
foreground: Color of the glyph on a tile (ignored when glyph-only).
|
|
404
|
+
A theme color token or a hex string. Defaults to `'white'`.
|
|
405
|
+
radius: Corner radius of the tile background as a fraction of the
|
|
406
|
+
icon size, from `0.0` (square) to `0.5` (circle). Ignored when
|
|
407
|
+
glyph-only. Defaults to `0.22`.
|
|
408
|
+
shape: `'auto'` (default) follows the platform and size — a tile for
|
|
409
|
+
macOS targets, and on Windows/Linux glyph-only at small sizes with
|
|
410
|
+
a tile at larger sizes. `'tile'` or `'glyph'` force one shape at
|
|
411
|
+
every size.
|
|
412
|
+
"""
|
|
413
|
+
self._icon = icon
|
|
414
|
+
self._background = background
|
|
415
|
+
self._foreground = foreground
|
|
416
|
+
self._radius = max(0.0, min(0.5, float(radius)))
|
|
417
|
+
self._shape = shape
|
|
418
|
+
self._cached_path: str | None = None
|
|
419
|
+
|
|
420
|
+
def _tiled_for_size(self, size: int, *, suffix: str | None = None) -> bool:
|
|
421
|
+
"""Decide whether to draw a tile for a given target size.
|
|
422
|
+
|
|
423
|
+
`shape='tile'`/`'glyph'` force the shape. With `shape='auto'`: macOS
|
|
424
|
+
targets (an `.icns` file, or — when no suffix is given — running on
|
|
425
|
+
macOS) are always a tile; Windows and Linux targets are glyph-only below
|
|
426
|
+
`_AUTO_TILE_MIN_SIZE` and a tile at or above it, so a multi-size `.ico`
|
|
427
|
+
carries bare marks for the title bar and tiles for the taskbar.
|
|
428
|
+
"""
|
|
429
|
+
if self._shape == "tile":
|
|
430
|
+
return True
|
|
431
|
+
if self._shape == "glyph":
|
|
432
|
+
return False
|
|
433
|
+
if suffix == ".icns" or (suffix is None and sys.platform == "darwin"):
|
|
434
|
+
return True
|
|
435
|
+
return size >= _AUTO_TILE_MIN_SIZE
|
|
436
|
+
|
|
437
|
+
def _render(self, size: int, *, tiled: bool) -> "_PILImage":
|
|
438
|
+
from PIL import Image as PILImage, ImageDraw
|
|
439
|
+
|
|
440
|
+
bg = _resolve_color(self._background)
|
|
441
|
+
fg = _resolve_color(self._foreground)
|
|
442
|
+
|
|
443
|
+
glyph_frac = _glyph_frac(size, tiled=tiled)
|
|
444
|
+
glyph_color = fg if tiled else bg
|
|
445
|
+
|
|
446
|
+
# Render the glyph at its FINAL on-screen size. A font glyph is crispest at
|
|
447
|
+
# its display size — the glyph renderer grid-fits it there and sharpens
|
|
448
|
+
# small sizes. If the glyph were supersampled with the tile and the whole
|
|
449
|
+
# composite downscaled, it would lose that and go soft at 16/24 px.
|
|
450
|
+
glyph_box = max(1, int(size * glyph_frac))
|
|
451
|
+
glyph = _ImageService._render_icon(self._icon, glyph_box, glyph_color, align=True)
|
|
452
|
+
|
|
453
|
+
def _center(base: "_PILImage") -> "_PILImage":
|
|
454
|
+
base.alpha_composite(
|
|
455
|
+
glyph, ((size - glyph.width) // 2, (size - glyph.height) // 2)
|
|
456
|
+
)
|
|
457
|
+
return base
|
|
458
|
+
|
|
459
|
+
if not tiled:
|
|
460
|
+
# Glyph-only: the brand-colored mark on a transparent field.
|
|
461
|
+
return _center(PILImage.new("RGBA", (size, size), (0, 0, 0, 0)))
|
|
462
|
+
|
|
463
|
+
# Tile: supersample ONLY the rounded background — its curved corners need
|
|
464
|
+
# antialiasing PIL won't apply at 1x — then downscale at an integer ratio
|
|
465
|
+
# for clean edges, and drop the native-size glyph on top.
|
|
466
|
+
scale = max(2, math.ceil(_MIN_TILE_CANVAS / size))
|
|
467
|
+
canvas = size * scale
|
|
468
|
+
tile = PILImage.new("RGBA", (canvas, canvas), (0, 0, 0, 0))
|
|
469
|
+
ImageDraw.Draw(tile).rounded_rectangle(
|
|
470
|
+
[0, 0, canvas - 1, canvas - 1],
|
|
471
|
+
radius=int(canvas * self._radius),
|
|
472
|
+
fill=bg,
|
|
473
|
+
)
|
|
474
|
+
tile = tile.resize((size, size), PILImage.Resampling.LANCZOS)
|
|
475
|
+
return _center(tile)
|
|
476
|
+
|
|
477
|
+
def to_image(self, size: int = 256, *, tiled: bool | None = None) -> "Image":
|
|
478
|
+
"""Render the icon to an `Image` handle for on-screen display.
|
|
479
|
+
|
|
480
|
+
Use this to show the application icon inside the app itself — for example
|
|
481
|
+
on an about screen or a splash panel.
|
|
482
|
+
|
|
483
|
+
Args:
|
|
484
|
+
size: Rendered size in pixels. Defaults to `256`.
|
|
485
|
+
tiled: Force the tile shape (`True`) or glyph-only (`False`). By
|
|
486
|
+
default, follows the icon's `shape`.
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
An image handle wrapping the rendered icon.
|
|
490
|
+
"""
|
|
491
|
+
size = int(size)
|
|
492
|
+
if tiled is None:
|
|
493
|
+
tiled = self._tiled_for_size(size)
|
|
494
|
+
return Image.from_pil(self._render(size, tiled=tiled))
|
|
495
|
+
|
|
496
|
+
def save(self, path: str | Path) -> str:
|
|
497
|
+
"""Write the icon to a file, choosing the format by file extension.
|
|
498
|
+
|
|
499
|
+
Use this to produce a packaging asset — for example, an icon to point a
|
|
500
|
+
PyInstaller build at. The format follows the path suffix:
|
|
501
|
+
|
|
502
|
+
- `.ico` — a multi-resolution Windows icon.
|
|
503
|
+
- `.icns` — a macOS icon.
|
|
504
|
+
- `.png` — a single 256-pixel image.
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
path: Destination file path ending in `.ico`, `.icns`, or `.png`.
|
|
508
|
+
|
|
509
|
+
Returns:
|
|
510
|
+
The path that was written, as a string.
|
|
511
|
+
|
|
512
|
+
Raises:
|
|
513
|
+
ValueError: If the file extension is not one of the supported types.
|
|
514
|
+
"""
|
|
515
|
+
return self._write(Path(path))
|
|
516
|
+
|
|
517
|
+
def _write(self, p: Path) -> str:
|
|
518
|
+
suffix = p.suffix.lower()
|
|
519
|
+
if suffix == ".ico":
|
|
520
|
+
# Each frame chooses its own shape: under `auto`, small frames are
|
|
521
|
+
# glyph-only and larger frames are tiled (see `_tiled_for_size`).
|
|
522
|
+
imgs = [
|
|
523
|
+
self._render(s, tiled=self._tiled_for_size(s, suffix=suffix))
|
|
524
|
+
for s in _APP_ICON_SIZES
|
|
525
|
+
]
|
|
526
|
+
# Largest first — Pillow skips sizes larger than the base image.
|
|
527
|
+
imgs[-1].save(
|
|
528
|
+
p,
|
|
529
|
+
format="ICO",
|
|
530
|
+
bitmap_format="bmp",
|
|
531
|
+
sizes=[(s, s) for s in _APP_ICON_SIZES],
|
|
532
|
+
append_images=imgs[:-1],
|
|
533
|
+
)
|
|
534
|
+
elif suffix == ".icns":
|
|
535
|
+
self._render(1024, tiled=self._tiled_for_size(1024, suffix=suffix)).save(
|
|
536
|
+
p, format="ICNS"
|
|
537
|
+
)
|
|
538
|
+
elif suffix == ".png":
|
|
539
|
+
self._render(256, tiled=self._tiled_for_size(256, suffix=suffix)).save(
|
|
540
|
+
p, format="PNG"
|
|
541
|
+
)
|
|
542
|
+
else:
|
|
543
|
+
raise ValueError(
|
|
544
|
+
f"unsupported app icon format {suffix!r}; use .ico, .icns, or .png"
|
|
545
|
+
)
|
|
546
|
+
return str(p)
|
|
547
|
+
|
|
548
|
+
def _icon_path(self) -> str:
|
|
549
|
+
"""Generate a runtime icon asset and return its file path.
|
|
550
|
+
|
|
551
|
+
On Windows a multi-resolution `.ico` is produced; elsewhere a `.png`.
|
|
552
|
+
The shape follows the icon's `shape` — under `auto`, a tile on macOS and,
|
|
553
|
+
on Windows/Linux, glyph-only for the small `.ico` frames and a tile for
|
|
554
|
+
the larger ones. Generated once per `AppIcon` and cached for reuse.
|
|
555
|
+
"""
|
|
556
|
+
if self._cached_path is not None:
|
|
557
|
+
return self._cached_path
|
|
558
|
+
suffix = ".ico" if sys.platform == "win32" else ".png"
|
|
559
|
+
fd, tmp = tempfile.mkstemp(suffix=suffix, prefix="bs-appicon-")
|
|
560
|
+
os.close(fd)
|
|
561
|
+
self._cached_path = self._write(Path(tmp))
|
|
562
|
+
_unlink_at_exit(self._cached_path)
|
|
563
|
+
return self._cached_path
|
bootstack/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Deferred and repeating task scheduling tied to a widget's lifetime.
|
|
2
|
+
|
|
3
|
+
Every public widget exposes a `Schedule` via its `schedule` property; each
|
|
4
|
+
scheduling call returns a cancellable `Job`.
|
|
5
|
+
"""
|
|
6
|
+
from bootstack.scheduling._schedule import Job, Schedule
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"Job",
|
|
10
|
+
"Schedule",
|
|
11
|
+
]
|