Showing Splash Screens#
A splash screen is the borderless intro window your app shows at startup, while
the main window is still being built. bootstack’s Splash is a
plain container you author like the app body — a logo, a title, a status line —
and the app reveals itself only once the splash dismisses.
This guide walks through the common splash shapes, from a plain intro that covers startup to a welcome screen that waits for the user, and the one timing rule that decides which patterns can show live motion.
The mental model#
A splash is its own window, not a panel inside the app. Constructing it inside
the App context registers it automatically; the app then defers showing itself
until the splash dismisses. Because it is a separate window it can appear
immediately, so where you write it decides how much of startup it covers —
put it first to cover the most.
import bootstack as bs
with bs.App(title="My App") as app:
with bs.Splash(min_duration=1.0): # appears now
bs.Picture(logo, width=140, height=140)
bs.Label("My App", font="heading-lg")
build_main_window() # covered by the splash
app.run() # app reveals when the splash closes
The with block scopes content authoring only — leaving it does not close the
splash. What closes it is the until rule, covered below.
The one timing rule: the event loop must turn#
This is the rule that shapes every pattern that follows, so it is worth stating
plainly: a splash can only show motion while the event loop is running. The
screen repaints — and a spinner or progress bar advances — only when the loop
gets a turn. The loop is not running during the synchronous code between your
Splash block and app.run().
So the default until="ready" splash, which covers exactly that synchronous
build, is a still image: a logo and a caption, frozen until the app is ready.
That is by design — there is no honest live progress to show for a block of
synchronous work. To show real motion you must hand the loop a turn, which the
patterns below do in two ways: a timed splash runs under the live loop, and a
progress splash moves the work off the main thread.
A plain intro that covers startup#
The default. until="ready" closes the splash the moment the app finishes
building. Add min_duration as an anti-blink floor so a fast startup still
shows the brand for a beat instead of flickering past.
with bs.App(title="My App") as app:
with bs.Splash(until="ready", min_duration=1.0):
bs.Picture(logo, width=140, height=140)
bs.Label("My App", font="heading-lg")
bs.Label("Loading…", accent="muted")
build_main_window()
app.run()
Keep the content static here — a spinner would sit frozen during the synchronous build (see the timing rule above).
Timed branding#
Give until a number of seconds for a splash that shows for a fixed time
regardless of how fast the app builds — a brand moment. App-ready does not
cut it short. Because the splash now sits on screen under the running event
loop (during app.run()), an indeterminate bar or spinner started with
start() animates here.
with bs.App(title="My App") as app:
with bs.Splash(until=2.5):
bs.Picture(logo, width=140, height=140)
bs.Label("My App", font="heading-lg")
bar = bs.ProgressBar(mode="indeterminate")
bar.start() # marches — the loop is live
build_main_window()
app.run()
A welcome screen that waits for the user#
until="manual" never closes on its own. Pair it with skippable=True (a
click or Escape dismisses it) or an authored button calling
dismiss(). This is the “press any key to continue”
welcome screen.
with bs.App(title="My App") as app:
splash = bs.Splash(until="manual")
with splash:
bs.Label("Welcome", font="heading-lg")
bs.Label("Ready when you are.", accent="muted")
bs.Button("Get started", accent="primary", on_click=splash.dismiss)
app.run()
Showing real progress#
For genuine, determinate progress you must move the slow work off the main thread
so the loop stays free to repaint. Run the work in a background thread, push
0 to 100 values into a Signal the bar is bound to,
update a status label the same way, and call dismiss()
when the work is done. Use until="manual" so the splash waits for your
signal, not the (instant) main-window build.
import threading
import time
import bootstack as bs
from bootstack.images import get_icon
def do_work(word):
"""Simulate a time-consuming task."""
time.sleep(len(word) * 0.5)
with bs.App(title="My App", size=(500, 500), vertical_items="center") as app:
bs.Label("Welcome to bootstack!", font="heading-lg")
progress = bs.Signal(0)
status = bs.Signal("Starting…")
splash = bs.Splash(until="manual", size=(500, 500))
with splash:
bs.Picture(get_icon("rocket", size=96, color="primary"), width=96, height=96)
bs.Label("My App", font="heading-lg")
bs.ProgressBar(signal=progress)
bs.Label(textsignal=status)
def load():
steps = ["settings", "plugins", "workspace"]
total = len(steps)
for i, step in enumerate(steps, start=1):
status.set(f"Loading {step}…")
do_work(step)
progress.set(int(i / total * 100))
progress.set(100)
splash.dismiss() # close when the real work finishes
threading.Thread(target=load, daemon=True).start()
app.run()
The progress bar uses the default max_value of 100, so the example sends
integer percentages with progress.set(...). The status label is updated with
status.set(...) because calling a signal reads its current value.
Reacting to dismissal#
on_dismiss() fires once as the splash begins to close,
with a SplashDismissEvent whose reason is
"ready", "timer", "manual", or "skip" — handy for kicking off a
first-run tip or recording that the user skipped the intro.
splash.on_dismiss(lambda e: print("entered the app via:", e.reason))
The min_duration floor applies under every pattern: whatever the trigger, the
splash never closes before it elapses.
See also#
Splash — the widget reference, every option in one place.
Picture — display a logo image, crisp on high-DPI displays.
ProgressBar — the bar used in the timed and progress patterns.
App Structures — the app shapes a splash introduces.