Movable panels

Panel owns an animation drawn in local coordinates, then places it in a parent buffer. It can frame the content, translate it, and bounce at explicit bounds. Use Scene to stack panels back to front. Layer.region remains a destination clip; Panel adds a separate local surface and translation.

Create a moving effect

Use Layouts and text widgets to arrange panels automatically in rows and columns, with typed Fixed/Flex sizing, labels, and status bars.

from bruhanimate import Panel, SnowEffect, effects, play

def build(buffer):
    return Panel(
        effect_factory=lambda local: SnowEffect(local, " ", effects.snow.preset("light")),
        size=(28, 8),
        position=(2, 2),
        velocity=(18, 3),
        bounds=(0, 0, buffer.width(), buffer.height()),
        bounce=True,
        border=True,
        title="Snow",
        border_color=117,
        updates_per_second=15,
    )

play(effect_factory=build, fps=60, updates_per_second=60, frames=180)

size=(28, 8) means 28 columns by 8 rows of content. With a border, the outer footprint is 30 columns by 10 rows. A child draws from local (0, 0); with a border, that cell appears one column and row inside the outer position. The child’s simulation is never resized or cropped internally when the panel moves. For example, fire starts at the bottom of its own local buffer.

Pass an existing animation for callbacks, images, solver playback, or a nested Scene/Panel. Its built-in effect buffer must already match the content size:

from bruhanimate import ImageEffect, Panel, Scene, play

back = Panel(ImageEffect("Back panel"), size=(20, 5), position=(2, 1), title="Back")
front = Panel(ImageEffect("Front panel"), size=(16, 4), position=(12, 3), title="Front")
play(Scene(back, front), frames=120)

Discovery and options

The constructor has explicit typed keyword options: type Panel( in your editor to browse them. Factories accept Buffer and return Animation; use typed settings classes or effects.snow.preset(...) to discover effect options and preset names. No untyped option dictionary is required.

Constructor options

Option

Meaning

animation / effect_factory

Supply exactly one. A factory runs once at start with a fresh local buffer.

size: tuple[int, int]

Required positive content width and height; excludes the border.

position: tuple[float, float]

Outer top-left in the parent’s coordinates. Defaults to (0, 0).

velocity: tuple[float, float]

Columns/second and rows/second. Defaults to stationary (0, 0).

bounds: tuple[int, int, int, int] | None

Parent-coordinate (x, y, width, height) clip. None uses only the destination buffer’s edges.

bounce: bool

Defaults to False. True requires bounds and reflects motion at their edges.

border: bool / title: str

ASCII border defaults to True. Plain single-cell title text is clipped to its top edge; a nonempty title requires a border.

border_color / border_background

Optional ANSI palette indices 0..255. None leaves that color unspecified.

background: str | None

One single-cell glyph, optionally SGR styled, for unpainted content. Defaults to a space. None allows transparency.

updates_per_second: float | None

Positive finite child update rate. None forwards every parent update.

fit: bool

False keeps fixed geometry; True follows parent resize notifications, retaining the child.

max_catch_up: int

Positive maximum child ticks per parent update; defaults to 5.

size, outer_size, position, velocity, animation, and finished are typed read-only properties. Position reports the current floating-point top-left; drawing places it on terminal cells. Velocity reports the configured rate before bounce reflection. animation is None before a factory runs. move_to((x, y)) changes position without advancing simulation. fit=True opts into parent viewport notifications; content dimensions become parent dimensions minus the border, with a minimum of one cell on each axis. Smaller allocations clip the border. A resize keeps the child and its clock alive. The default fit=False preserves fixed panel geometry. Bounds remain explicit clips even for fitting panels. See Terminal resize.

focus=FocusTarget(...) attaches typed keyboard bindings and selection state. Use FocusGroup(...).handle_event on Player to route input. The selected panel draws a bold border with focus_border_color=226 by default; None keeps its normal color. set_visible(False) hides painting and removes its nested targets from focus, while simulation and completion continue. Layouts propagate slot visibility automatically. Arbitrary clipping and overlap do not change focus availability; see Keyboard bindings and focus for routing and ownership rules. Wrap a larger animation in ScrollView or use LogView as the child for scrollable text history. A fitting panel resizes their viewports while preserving content and scroll state. See Scrollable views and activity logs for typed options and bindings.

Clipping, motion, and stacking

Negative positions are allowed. Bounds clip the entire panel, including its frame, but never resize its local buffer. Bounds have integer coordinates and non-negative dimensions; an empty bounds rectangle hides the panel. Offscreen and hidden panels keep updating. An outer Layer can clip the result again.

For bouncing, the initial position clamps inside bounds. The full outer footprint stays inside the rectangle if it fits. An oversized axis anchors at the bounds origin and clips. Large updates can reflect across several edges. Bounds are explicit so motion does not depend on draw being called or on the last target buffer. Nested panels use their parent’s local content coordinates.

Motion advances only in update(dt), in supplied simulation seconds. The child’s fixed-rate Layer waits for a complete interval before its first tick. Reaching max_catch_up discards leftover child time, including the fractional remainder; panel motion still advances by the full dt. Throttling an outer Layer also throttles delivery to Panel, so prefer Panel’s own rate to keep movement smooth while its effect updates less often. See Panels, layer clocks, and temporary overlays for timing.

Later Scene children paint over earlier ones. None cells from the child reveal the panel background; use background=None to reveal lower layers through unpainted cells. Plain child spaces always paint unless an outer Layer opts into space transparency. Styling stays inside individual cells. Every frame composes the current positions, so moving windows leave no trails and do not mutate previously presented buffers.

Ownership, completion, and input

Panel owns its supplied or factory-created animation. Do not put that child in another owner or share its mutable buffer. Construction performs validation but does not call the factory, start content, or acquire terminal/device resources. Use the factory form to avoid manually matching built-in effect dimensions.

Start is idempotent while open. Close is idempotent, closes acquired content on failed startup, and also closes a supplied child even if never started. Closing an unstarted factory-backed panel does not call the factory. After close or a startup failure, construct a new panel; update/draw require an open, started instance. Player provides cleanup when update/draw fails. A factory that raises before returning a child remains responsible for its own partial acquisitions.

Completion mirrors the child: None means endless or a factory not yet called. Scene and Player honor finite completion normally. on_complete="hold" keeps moving and presenting a completed panel; capture defaults to exit on completion. For a whole temporary window, wrap Panel in TimedOverlay. Wrapping only its child hides that content on expiry but leaves the panel’s frame and background.

Panel does not poll keyboard events. Pass a FocusGroup’s or solver’s handler to Player/play explicitly. There are no extra loops, sleeps, threads, or terminal handles. The Moving windows demo example uses four Panel instances at independent rates and retains its faster bouncing movement.

API

Owned local animation surfaces with positioning, framing, and motion.

class bruhanimate.panel.Panel(animation: Animation | None = None, *, size: tuple[int, int], effect_factory: Callable[[Buffer], Animation] | None = None, position: tuple[float, float] = (0, 0), velocity: tuple[float, float] = (0, 0), bounds: tuple[int, int, int, int] | None = None, bounce: bool = False, border: bool = True, fit: bool = False, focus: FocusTarget | None = None, focus_border_color: int | None = 226, title: str = '', border_color: int | None = None, border_background: int | None = None, background: str | None = ' ', updates_per_second: float | None = None, max_catch_up: int = 5)[source]

Own an animation in a movable, clipped local content area.

size=(width, height) is the positive content size, excluding the border. Supply exactly one of animation or effect_factory. The factory runs once at start with a fresh content-sized Buffer; built-in effect buffers must match size. Construction is inert. The panel owns the child, closes it on cleanup/failed startup, and is single-use after close. Never share a child between owners. A supplied child is also closed if the panel never starts.

position is the outer top-left in parent coordinates. velocity is columns/second and rows/second. Motion advances only in update(dt). Bounds are an optional parent-coordinate (x, y, width, height) clip. bounce=True requires bounds, reflects motion at their edges, and clamps the initial position inside them. An oversized axis stays at the corresponding origin. Bounds never resize the child. Scene order determines stacking.

Border uses ASCII geometry; title is plain single-cell text, clipped to the top edge, and requires border=True. Optional border colors are ANSI palette indices 0..255. Background is one optionally SGR-styled cell, or None for transparency. Child spaces paint; child None cells reveal the background.

updates_per_second paces only the child via Layer; None forwards every update. max_catch_up has Layer’s discard-on-cap behavior. Motion still uses all supplied dt. Completion mirrors the child; input is not forwarded. Pass handlers to Player explicitly. fit=True follows parent resize notifications; the default keeps fixed geometry. focus attaches one FocusTarget for keyboard routing through FocusGroup.handle_event. Its border becomes bold with focus_border_color (None retains its normal color). Drag handling is deferred.

__init__(animation: Animation | None = None, *, size: tuple[int, int], effect_factory: Callable[[Buffer], Animation] | None = None, position: tuple[float, float] = (0, 0), velocity: tuple[float, float] = (0, 0), bounds: tuple[int, int, int, int] | None = None, bounce: bool = False, border: bool = True, fit: bool = False, focus: FocusTarget | None = None, focus_border_color: int | None = 226, title: str = '', border_color: int | None = None, border_background: int | None = None, background: str | None = ' ', updates_per_second: float | None = None, max_catch_up: int = 5) → None[source]
set_visible(visible: bool) → None[source]

Set painting/focus availability without pausing or closing content.

Layouts propagate empty-slot and ancestor visibility automatically. This does not infer occlusion or test moving panels against screen edges.

move_to(position: tuple[float, float]) → None[source]

Move the outer origin without advancing content; bounce panels clamp it.

resize(size: tuple[int, int]) → None[source]

Receive parent (width, height); fit=True fills it, excluding borders.

Fixed panels retain geometry. Fitting panels retain their child and clocks. A border in a viewport smaller than three cells is clipped; the private content keeps at least one cell on each axis. Bounds remain explicit parent-coordinate clips and are not changed by notifications.

property size: tuple[int, int]

Content (width, height), excluding the optional border.

property outer_size: tuple[int, int]

Full footprint (width, height), including the optional border.

property velocity: tuple[float, float]

Configured motion in columns/second and rows/second, before reflection.

property position: tuple[float, float]

Current outer top-left in parent coordinates; drawing floors to cells.

property animation: Animation | None

Owned child; None until startup when using effect_factory. Do not re-own it.

property finished: bool | None

Child completion; None for endless content or an uncalled factory.

start() → None[source]

Create/acquire the child once, closing it if startup fails.

update(dt: float) → None[source]

Advance motion by simulation seconds and pace the child independently.

draw(buffer: Buffer) → None[source]

Translate current local pixels into a clipped parent area, without updates.

close() → None[source]

Release owned content once, including before start and after failures.