overlay

package module
v0.0.0-...-97d5221 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 28, 2026 License: MIT Imports: 10 Imported by: 0

README

bubble-overlay

Composable modals and overlay stacks for Bubble Tea: v1 (View() string + OverlayView / OverlayStack) and v2 (View() tea.View + overlayv2).

Simple Demo

Requirements

  • Go 1.25+ (this module includes charm.land/bubbletea/v2 alongside Bubble Tea v1.)

Installation

go get github.com/madicen/bubble-overlay

The v2 API lives at import path github.com/madicen/bubble-overlay/v2 (package overlayv2); you still add the root module once.

Which API should I use?

You are on… Use
Bubble Tea v1, View() string Package overlay: OverlayView, OverlayStack, OverlayConfig, FocusTrap.
Bubble Tea v2 (charm.land/bubbletea/v2), View() tea.View Package overlayv2: Stack, CompositeView, shared config from overlay.

Both paths use the same string compositing for the terminal (overlay.OverlayView, grapheme-aware, ANSI/hyperlink-safe). The v2 stack flattens each child tea.View, composites, then wraps with tea.NewView — see docs/ADR-v2-bridge.md.


Bubble Tea v1 — quick start

import overlay "github.com/madicen/bubble-overlay"

func (m model) View() string {
	main := m.renderMain()
	if !m.showModal {
		return main
	}
	modal := lipgloss.NewStyle().Width(40).Render("…")
	return overlay.OverlayView(main, modal, m.width, m.height, row, col)
}

OverlayStack adds nested modals, dimming, Center / RightDrawer / Fixed, Escape and optional click-outside, optional WindowChrome (title bar, drag, close), and FocusTrap so your base model skips keys/mouse while overlays are open. See examples/simple, examples/confirm, examples/stack, examples/draggable.


Bubble Tea v2 — quick start

import (
	tea "charm.land/bubbletea/v2"
	bov "github.com/madicen/bubble-overlay"
	"github.com/madicen/bubble-overlay/v2"
)

func (m rootModel) View() tea.View {
	w, h := m.width, m.height
	if w == 0 || h == 0 {
		w, h = 80, 25
	}
	v := m.stack.CompositeView(m.mainView, w, h)
	v.AltScreen = true
	return v
}

Run go run examples/v2simple/main.go. Use overlayv2.FocusTrap like v1 FocusTrap for interactive routing.


Usage

bubble-overlay composites a string modal over a string main view without destroying the background. It uses display-cell width (grapheme-aware, aligned with lipgloss) and handles ANSI correctly: a full SGR reset (and hyperlink reset) is inserted immediately before the modal so the main line’s active pen does not bleed into the dialog, then after the modal the pen that belonged at the right edge of the hole is re-applied so long styled runs (e.g. full-width lipgloss bars) still look correct past the cut.

examples/colors shows two styled rows with the modal cutting through them; toggle with space.

Transparency and mask
  • OverlayViewWithTransparency: ASCII space (' ') in the modal is treated as transparent so the main view shows through at those cells.
  • OverlayViewWithMask: choose a mask rune (e.g. ); only those cells pass through to the main view — see examples/transparency.

Centering. OverlayViewInCenter(main, modal, viewW, viewH) centers the modal in an arbitrary viewport—full terminal, tab strip, or panel—not “full screen only”. Pass the same viewW / viewH you use when compositing that region. When the region’s bounds match the main string’s grid, use ModalCellSize(main) for viewW / viewH, or call OverlayViewInCenterInMain(main, modal) which does that for you.

Centering helpers measure the modal with ModalCellSize (same rules as OverlayView), not lipgloss.Size, so placement and hit-testing stay aligned.

Context menus: use OverlayViewAtPoint(main, modal, viewW, viewH, anchorTop, anchorLeft) (clamp + composite in one step), or ClampOverlayOriginAtPoint / ClampMenuOrigin + OverlayView. Test hits with CellInModal using post-clamp top/left and ModalCellSize(modal).

For “centered but nudged” (e.g. loading line above a label), use OverlayViewInCenterWithOffset (and transparency/mask variants): offsets apply after centering, then OverlayView clamps.

Helpers OverlayViewInCenter*, OverlayViewInCenterInMain, OverlayViewAtPoint*, and OverlayViewInCenterWithOffset* cover common layouts.

Cookbook

Full-screen vs inner panel. Use WindowSizeMsg width/height as viewW / viewH when the main view fills the terminal. When the overlay sits only over a panel whose string is exactly main, use OverlayViewInCenterInMain(main, modal) or ModalCellSize(main) with OverlayViewInCenter so the viewport matches the panel grid.

Menu under cursor (v1 mouse). Coordinates are zero-based. tea.MouseMsg uses X = column (left) and Y = row (top)—same order as OverlayView(..., top, left) arguments only if you pass anchorTop = msg.Y and anchorLeft = msg.X (row first, then column). Example:

out := overlay.OverlayViewAtPoint(base, menu, w, h, msg.Y, msg.X)
t, l := overlay.ClampMenuOrigin(menu, w, h, msg.Y, msg.X)
mw, mh := overlay.ModalCellSize(menu)
inside := overlay.CellInModal(msg.X, msg.Y, t, l, mw, mh)

Stack vs raw compositing. Prefer OverlayStack / Placement when you want dimming, Escape / click-outside, nested modals, and focus routing (FocusTrap). Use OverlayView (and helpers) when you only need a single hole punch or fully custom update routing.

Draggable window chrome. Set OverlayConfig.WindowChrome (or EnableWindowChrome(title)) to add a bordered tab (offset down/right), drag-by-tab, and an optional [x] close control. Set Resizable: true to drag the right edge, bottom edge, or corner to resize (content should be unframed; chrome draws the border). Set Keyboard: true for Alt+arrow move and Alt+Shift+arrow resize (KeyStep sets cells per keypress, default 1). The title sits on the window’s top edge (│ title ┴────┐), with a small tab cap (┌───┐) on the row above. Customize TabBackground, TabForeground, TabBorder, CenterContent, ContentPadTop, MinWidth, MinHeight, and ChromeMaskRune (default DefaultChromeMaskRune = U+E000 in the Private Use Area, so it can't collide with real content; override if your content actually emits PUA runes — pass-through padding via OverlayViewWithMask). The stack auto-wraps the overlay’s View() unless you call WindowFrame yourself and set AutoWrap: false. Enable mouse in your program (tea.WithMouseAllMotion() on v1). Call stack.View(main, w, h) with the same w/h you use for WindowSizeMsg so chrome hit-testing stays aligned. See examples/draggable.

Stack-pushed model hooks. Stack entries can opt into three small interfaces:

Interface When the stack calls it What it's for
OverlayOnCloser (OnOverlayClose() tea.Cmd) After Pop() — including the [x] close button, Escape (when CloseOnEscape is on), and click-outside dismissal Run the same cleanup you'd run on user-initiated dismissal (clear cached state, refresh background views, etc.)
OverlayTitler (OverlayTitle() string) Every render frame Drive a dynamic tab title (e.g. "review · running…" → "…approved") without rebuilding the OverlayConfig. Return "" to fall back to cfg.WindowChrome.Title.
OverlayResizer (OnOverlayResize(w, h int) tea.Cmd) After a chrome resize gesture (mouse release on a resize edge, Alt+Shift keyboard grow) Hook the moment when the content rect changes. The stack also delivers a parallel OverlayResizedMsg{NewContentWidth, NewContentHeight} through the model's own Update, so consumers can choose whichever signal fits their architecture.

bubblezone compatibility. Modal rows that contain no transparent cells (the common case for chromed bodies, since WindowChrome.AutoWrap only pads padding rows with the mask rune) take a string-splice fast path in overlayLine. That path concatenates the modal substring verbatim, so zero-width CSI sequences like bubblezone's \x1B[<id>z markers survive compositing intact — bubblezone.Scan over stack.View(main, w, h) will still find your zones. Transparent cells force the cellbuf compositor, which drops unknown zero-width sequences as a side effect of re-emitting decoded cell data; keep zone markers out of rows that you intentionally make transparent.

Pass-through mouse routing. Hosts that want their main view to stay clickable while a modal is open (e.g. a long-running progress overlay where the user should still be able to browse the underlying content) can call stack.MouseTargetsTop(msg, w, h) before forwarding to stack.Update. It returns true when the event belongs to the top overlay — coordinates inside the painted modal rect, or any motion / release while a chrome drag or resize gesture is in progress — and false when the host should route the message to its own main model instead. Keyboard events are unaffected; they still go through stack.Update as before, so the modal keeps owning its keymap (Escape, action keys, etc.).

Minimize button. Set WindowChrome.ShowMinimizeButton = true to render a [-] toggle to the left of the close button. Clicking it flips LayerState.Minimized and collapses the window to its tab strip (cap + tab row + flat bottom border) — the body and resize handles disappear, but the chrome stays draggable and the title (including any OverlayTitler dynamic value) stays visible. The glyph flips to [+] when minimized; clicking it again restores the body to its previous content size. Both glyphs share MinimizeButtonWidth so the close button doesn't shift columns between states. The stack notifies the entry model when state toggles, via both an OverlayMinimizedMsg{Minimized: bool} delivered through Update and an optional OverlayMinimizer.OnOverlayMinimize(bool) callback — same dual-signal pattern as OverlayResizer / OverlayResizedMsg.

Double-click to minimize / restore. When ShowMinimizeButton is enabled, two presses on the tab drag area within DoubleClickThreshold (500ms) toggle minimize — mirroring the OS-level title-bar gesture. The chrome cancels the drag the first press kicked off so the window doesn't drift while toggling. The gesture is gated on ShowMinimizeButton for discoverability: without the visible [-]/[+] affordance, a hidden double-click action would be too surprising.

Window (single-modal helper). When you don't need stack semantics, use the Window type from pane.go: pass content, title, and a stable key and it manages chrome, drag, resize, and close for you. Set Window.Configure if you need to override the built-in defaults (CenterContent, MinWidth=32, MinHeight=6, default mask rune): the callback receives the populated OverlayConfig and may mutate any field; WindowChrome.Enabled is forced back on so the render path stays consistent.


Consumer integration (OverlayView hosts)

Single source of truth. Overflow clamping (when the modal is wider or taller than the viewport) is implemented once as ClampOverlayOrigin and used by OverlayView. Hosts that duplicate placement logic for hit-testing should call ClampOverlayOrigin or Placement.ClampedOrigin with the same modalW, modalH, viewW, and viewH they use for compositing—do not reimplement the algorithm.

Placement. Placement.Origin returns coordinates before that overflow clamp (it only pins negative top/left to zero). Placement.ClampedOrigin matches what OverlayView paints. Use ClampedOrigin (or Origin plus ClampOverlayOrigin) whenever coordinates must align with the compositor.

Hit-testing. If you forward tea.MouseMsg (or v2 mouse messages) and compare against a stored overlay rectangle, that rectangle must use post-clamp top/left; comparing against pre-clamp “desired” placement will be wrong when the modal overflows the viewport.

Coordinates. OverlayView top/left are zero-based row and column offsets from the top-left of the view string. Bubble Tea v1 tea.MouseMsg X and Y use the same zero-based cell indexing, so they align directly with ClampOverlayOrigin / CellInModal. For Bubble Tea v2, use the X / Y from the underlying mouse event the same way once your pipeline uses the same width/height as compositing.

Helpers. ModalCellSize, CellInModal, CellInTitleBar, and CellInCloseButton are thin exports over the same helpers used by OverlayStack for modal bounds and chrome hit-testing.

Behavioral note (sizing). Modal width/height follow strings.Split(modal, "\n") and max lipgloss.Width per line (matching OverlayView), not a trimmed trailing newline. If you change that measurement in the compositor, update internal/layout.ModalCellSize and release notes accordingly.

Checklist when changing the compositor

Before merging overlay geometry or merge behavior changes: exercise resize, modal larger than the terminal, mouse inside vs outside the modal, and zones vs relative coordinates if your app uses them. Call out breaking behavioral changes in release notes (this repo has no auto-generated changelog—document in your release).


Package reference

Symbol Package Role
OverlayView, OverlayViewWithTransparency, OverlayViewWithMask, DimSurface overlay Hole-punch compositing; dim multiline string.
ClampOverlayOrigin, ClampOverlayOriginAtPoint, ClampMenuOrigin, ModalCellSize, CellInModal overlay Shared geometry for compositor parity and hit-testing.
OverlayViewInCenter*, OverlayViewInCenterInMain, OverlayViewInCenterWithOffset*, OverlayViewAtPoint* overlay Common centered, offset, and anchored layouts.
OverlayConfig, Placement, Placement.ClampedOrigin overlay Per-frame dimming and anchor.
WindowChrome, EnableWindowChrome, HandleChromeKey, WindowFrame, RenderEntryModal overlay Title bar, drag, resize, keyboard chrome, and framing helpers.
OverlayStack, OverlayStack.MouseTargetsTop, OverlayOnCloser, OverlayTitler, OverlayResizer, OverlayResizedMsg, OverlayMinimizer, OverlayMinimizedMsg, FocusTrap, DevStackDepthFooter overlay v1 stack, lifecycle hooks, mouse-routing hit-test, and helpers.
Window, Window.Configure, WindowResizedMsg overlay Single-modal helper for state-machine-driven apps.
Stack, ViewAdapter, StringPipelineAdapter, ViewString overlayv2 v2 stack + R1 compositor.

Examples

Example Bubble Tea What it shows
examples/simple v1 OverlayStack, center + dim
examples/confirm v1 Yes/no + cmd + Pop
examples/stack v1 Nested overlays, OVERLAY_DEV=1 footer
examples/form v1 OverlayView + text input
examples/spinner v1 OverlayView + spinner
examples/colors v1 Styled lines through the modal cut
examples/transparency v1 Mask rune pass-through
examples/draggable v1 WindowChrome, title-bar drag, ✕ close
examples/v2simple v2 overlayv2.Stack + CompositeView
go run examples/simple/main.go
go run examples/confirm/main.go
go run examples/form/main.go
go run examples/spinner/main.go
go run examples/colors/main.go
go run examples/transparency/main.go
go run examples/draggable/main.go
go run examples/stack/main.go
OVERLAY_DEV=1 go run examples/stack/main.go
go run examples/v2simple/main.go

Confirm Form Spinner Colors Nested stack Transparency Draggable
Confirm Form Spinner Colors Stack Transparency Draggable

Recording GIFs (VHS)

Use make gifs from the repo root. Tapes Source "vhs/_env.tape", which clears CI / NO_COLOR and sets TERM=xterm-256color and COLORTERM=truecolor so lipgloss/termenv emit color (termenv otherwise assumes no TTY when CI is set).

Each tape runs go mod download inside Hide before go run ./examples/... so download lines do not appear in the GIF.

Documentation

Index

Constants

View Source
const CloseButtonGlyph = "[x]"

CloseButtonGlyph is the close control label inside the tab.

View Source
const CloseButtonWidth = 3

CloseButtonWidth is the display width of CloseButtonGlyph for hit-testing.

View Source
const DefaultChromeMaskRune = '\ue000'

DefaultChromeMaskRune is the pass-through padding rune for WindowChrome auto-wrap. It lives in the Unicode Private Use Area (U+E000), which is guaranteed not to appear in normal text — glamour-rendered markdown, terminal images, and other rich content occasionally include U+FFFC (OBJECT REPLACEMENT CHARACTER), which previously caused mergeMaskRune to punch transparent holes through chromed modals.

Set WindowChrome.ChromeMaskRune to override if your content happens to produce U+E000 (extremely unlikely outside custom font pickers).

View Source
const DoubleClickThreshold = 500 * time.Millisecond

DoubleClickThreshold is the maximum wall-clock gap between two chrome tab presses for the chrome handler to treat the second press as a double-click. 500ms matches the OS-level default on every major desktop platform, which is the gesture this feature mimics (double-click title bar to minimize / maximize).

Consumers that need a different threshold can override it via LayerState.LastTabPressAt manipulation, but the constant itself is intentionally hard-coded so the gesture feels uniform across apps built on this library.

View Source
const MinimizeButtonGlyph = "[-]"

MinimizeButtonGlyph is the label shown for the minimize control when the window is expanded. Clicking it collapses the window down to its tab.

View Source
const MinimizeButtonWidth = 3

MinimizeButtonWidth is the display width of MinimizeButtonGlyph (and RestoreButtonGlyph — they're sized identically for layout stability).

View Source
const RestoreButtonGlyph = "[+]"

RestoreButtonGlyph is the label shown in place of the minimize control when the window is collapsed. Clicking it expands the body back to the last content size. The two glyphs share a width so the close button to their right doesn't shift between states.

Variables

This section is empty.

Functions

func CellInCloseButton

func CellInCloseButton(x, y, top, left, mw, titleBarH, closeW int) bool

CellInCloseButton reports whether terminal cell coordinates (x, y) fall on the close control in the title bar's rightmost closeW columns. Use the same placement and chrome dimensions as CellInTitleBar and CellInModal so click handling matches OverlayStack.

@param x zero-based column of the cell to test. @param y zero-based row of the cell to test. @param top compositor row of the modal's top-left corner. @param left compositor column of the modal's top-left corner. @param mw modal width in cells (same as ModalCellSize width). @param titleBarH number of title-bar rows from top (must be > 0 for a hit). @param closeW width in cells of the close control (must be > 0 for a hit).

@return true when (x, y) lies in the close region [left+mw-closeW, left+mw) × [top, top+titleBarH).

func CellInModal

func CellInModal(x, y, top, left, mw, mh int) bool

CellInModal reports whether terminal cell coordinates (x, y) fall inside the modal rectangle [left, left+mw) × [top, top+mh). Use coordinates after ClampOverlayOrigin / Placement.ClampedOrigin so hit-testing matches painted overlay geometry.

Bubble Tea v1 tea.MouseMsg uses zero-based X and Y for the terminal cell (column, row), matching top and left passed into OverlayView (also zero-based from the top-left of the view).

func CellInTitleBar

func CellInTitleBar(x, y, top, left, mw, titleBarH, closeW int) bool

CellInTitleBar reports whether terminal cell coordinates (x, y) fall on the title bar at the top of a modal, excluding the close-button strip on the right when closeW > 0. Use the same top, left, mw, and titleBarH values as compositing and chrome hit-testing (for example from ComputeChromeRegions or WindowChrome titleBarHeight) so results match painted geometry.

Bubble Tea v1 tea.MouseMsg uses zero-based X and Y for the terminal cell (column, row), matching top and left passed into OverlayView.

@param x zero-based column of the cell to test. @param y zero-based row of the cell to test. @param top compositor row of the modal's top-left corner. @param left compositor column of the modal's top-left corner. @param mw modal width in cells (same as ModalCellSize width). @param titleBarH number of title-bar rows from top (0 means no title bar). @param closeW width in cells of the close control on the right (0 if absent).

@return true when (x, y) lies in [left, left+mw) × [top, top+titleBarH) and not in the close region.

func ClampMenuOrigin

func ClampMenuOrigin(modal string, viewW, viewH, anchorTop, anchorLeft int) (int, int)

ClampMenuOrigin is an alias for ClampOverlayOriginAtPoint: same implementation, for call sites that anchor a menu or popover at a cursor cell (typically tea.MouseMsg.Y as top, tea.MouseMsg.X as left).

func ClampOverlayOrigin

func ClampOverlayOrigin(modalW, modalH, viewW, viewH, top, left int) (int, int)

ClampOverlayOrigin applies the same origin adjustment as OverlayView: if the modal rectangle would extend past the viewport edge, top and/or left are shifted so the rectangle fits; then negative coordinates are clamped to zero.

modalW and modalH must match how OverlayView measures that modal string (see ModalCellSize).

func ClampOverlayOriginAtPoint

func ClampOverlayOriginAtPoint(modal string, viewW, viewH, top, left int) (int, int)

ClampOverlayOriginAtPoint runs ModalCellSize(modal) then ClampOverlayOrigin. Use for anchors such as a context menu at (top, left) (e.g. Bubble Tea mouse Y/X as row/column) so hit-testing with CellInModal matches OverlayView(modal, …, top, left).

func ComposeModalLayer

func ComposeModalLayer(cur, modal string, cfg OverlayConfig, top, left, viewW, viewH int) string

ComposeModalLayer composites a framed modal over cur using mask pass-through when chrome is enabled.

func ContentSizeForLayer

func ContentSizeForLayer(modal string, wc WindowChrome, layer *LayerState) (w, h int)

ContentSizeForLayer returns the content dimensions used for layout and hit-testing.

func DevStackDepthFooter

func DevStackDepthFooter(depth int, dev bool) string

func DimSurface

func DimSurface(s string, opacity float64) string

func EntryClampedOrigin

func EntryClampedOrigin(cfg OverlayConfig, st *LayerState, modal string, viewW, viewH int) (top, left int)

EntryClampedOrigin returns the compositor origin for an entry after seeding from Placement when needed.

func InitLayerContentSize

func InitLayerContentSize(layer *LayerState, modelView string, wc WindowChrome)

InitLayerContentSize seeds layer content dimensions from a model view on Push.

func ModalBodyHeight

func ModalBodyHeight(modal string, wc WindowChrome) int

ModalBodyHeight returns the content line count below the tab chrome.

func ModalBodyWidth

func ModalBodyWidth(modal string, wc WindowChrome) int

ModalBodyWidth returns the display width of content lines below the tab chrome.

func ModalCellSize

func ModalCellSize(modal string) (w, h int)

ModalCellSize returns display-cell width (max per line) and line count for a modal string, using the same rules as OverlayView when measuring the modal for placement and clamping.

func MutedTabBackground

func MutedTabBackground(border string) string

MutedTabBackground returns a fill color slightly darker than a 6×6×6 border index, lowering the strongest channel(s) so the hue stays aligned with the border. Non-cube values (hex names, grays) fall back to defaultTabBackground.

func OverlayView

func OverlayView(mainView, modalView string, viewWidth, viewHeight, top, left int) string

OverlayView composites modalView on top of mainView. Only the rectangle at (top, left) with the modal's size is replaced; all other cells show the main view. Returns a single string with viewHeight lines, each viewWidth cells wide (padding/truncation as needed).

Main and modal strings may contain ANSI (e.g. from lipgloss); overlay uses display-cell width (grapheme-aware, matching lipgloss) so alignment is correct. After the modal, graphics state that originated under the modal is re-applied so background colors and other SGR attributes still apply to the visible tail of each line. A full SGR reset (and hyperlink reset) is inserted immediately before the modal so the main line’s active pen does not bleed into the first cells of the modal.

func OverlayViewAtPoint

func OverlayViewAtPoint(mainView, modalView string, viewWidth, viewHeight, anchorTop, anchorLeft int) string

OverlayViewAtPoint composites modalView with its top-left anchored at (anchorTop, anchorLeft) after the same overflow clamp as OverlayView. Use Bubble Tea v1 mouse coordinates as anchorTop = msg.Y (row) and anchorLeft = msg.X (column).

func OverlayViewAtPointWithMask

func OverlayViewAtPointWithMask(mainView, modalView string, viewWidth, viewHeight, anchorTop, anchorLeft int, maskRune rune) string

OverlayViewAtPointWithMask is like OverlayViewAtPoint but uses OverlayViewWithMask.

func OverlayViewAtPointWithTransparency

func OverlayViewAtPointWithTransparency(mainView, modalView string, viewWidth, viewHeight, anchorTop, anchorLeft int) string

OverlayViewAtPointWithTransparency is like OverlayViewAtPoint but uses OverlayViewWithTransparency.

func OverlayViewInCenter

func OverlayViewInCenter(mainView, modalView string, viewWidth, viewHeight int) string

OverlayViewInCenter centers modalView in a viewport of viewWidth×viewHeight and composites with OverlayView. The viewport may be the full terminal, a tab/content region, or any rectangle you pass to OverlayView—this is the general “center in viewport” helper, not full-screen-only.

When the background is not the entire terminal, pass the same width and height you use for that region. If the region matches the main view’s cell bounds, use ModalCellSize(mainView) or OverlayViewInCenterInMain.

Centering uses ModalCellSize(modalView), matching OverlayView’s internal modal measurement (not lipgloss.Size).

func OverlayViewInCenterInMain

func OverlayViewInCenterInMain(mainView, modalView string) string

OverlayViewInCenterInMain derives the viewport size from ModalCellSize(mainView) and centers modalView over mainView. Use when the overlay applies to exactly the main string’s cell bounds (e.g. an inner panel or log region).

func OverlayViewInCenterWithMask

func OverlayViewInCenterWithMask(mainView, modalView string, viewWidth, viewHeight int, maskRune rune) string

OverlayViewInCenterWithMask is like OverlayViewInCenter but uses OverlayViewWithMask.

func OverlayViewInCenterWithOffset

func OverlayViewInCenterWithOffset(mainView, modalView string, viewWidth, viewHeight, deltaTop, deltaLeft int) string

OverlayViewInCenterWithOffset centers modalView, adds deltaTop and deltaLeft (e.g. nudge a loading banner upward), then composites. Overflow clamping matches OverlayView (applied inside OverlayView).

func OverlayViewInCenterWithOffsetWithMask

func OverlayViewInCenterWithOffsetWithMask(mainView, modalView string, viewWidth, viewHeight, deltaTop, deltaLeft int, maskRune rune) string

OverlayViewInCenterWithOffsetWithMask is like OverlayViewInCenterWithOffset but uses OverlayViewWithMask.

func OverlayViewInCenterWithOffsetWithTransparency

func OverlayViewInCenterWithOffsetWithTransparency(mainView, modalView string, viewWidth, viewHeight, deltaTop, deltaLeft int) string

OverlayViewInCenterWithOffsetWithTransparency is like OverlayViewInCenterWithOffset but uses OverlayViewWithTransparency.

func OverlayViewInCenterWithTransparency

func OverlayViewInCenterWithTransparency(mainView, modalView string, viewWidth, viewHeight int) string

OverlayViewInCenterWithTransparency is like OverlayViewInCenter but uses OverlayViewWithTransparency.

func OverlayViewWithMask

func OverlayViewWithMask(mainView, modalView string, viewWidth, viewHeight, top, left int, maskRune rune) string

OverlayViewWithMask is like OverlayView but treats any cell whose rune equals maskRune as transparent (pass-through to the main view). Use 0 for maskRune to behave like OverlayView.

func OverlayViewWithTransparency

func OverlayViewWithTransparency(mainView, modalView string, viewWidth, viewHeight, top, left int) string

OverlayViewWithTransparency is like OverlayView but cells in the modal that are ASCII space (' ') are treated as transparent: the main view shows through at those positions. Non-space modal cells (including styled spaces from lipgloss that carry a background) still replace the main view.

func ReclampLayerOrigin

func ReclampLayerOrigin(cfg OverlayConfig, st *LayerState, modal string, viewW, viewH int)

ReclampLayerOrigin clamps a draggable origin after viewport resize.

func RenderEntryModal

func RenderEntryModal(modelView string, cfg OverlayConfig, layer *LayerState) string

RenderEntryModal returns the modal string for an entry, including auto-wrap chrome when configured.

func WindowFrame

func WindowFrame(content, title string, opts WindowFrameOpts) string

WindowFrame renders a tab-style title and bordered content. Width defaults to content width.

Types

type ChromeKeyResult

type ChromeKeyResult struct {
	Consumed bool
	Pop      bool
}

ChromeKeyResult describes stack handling of a key message on window chrome.

func HandleChromeKey

func HandleChromeKey(msg tea.KeyMsg, cfg OverlayConfig, st *LayerState, modal string, top, left, mw, mh, viewW, viewH int) ChromeKeyResult

HandleChromeKey handles Alt+arrow move and Alt+Shift+arrow resize when Keyboard is enabled.

func HandleChromeKeyLayout

func HandleChromeKeyLayout(msg tea.KeyMsg, cfg OverlayConfig, st *LayerState, layout ChromeLayout, viewW, viewH int) ChromeKeyResult

HandleChromeKeyLayout is like HandleChromeKey but uses a precomputed ChromeLayout.

func HandleChromeKeyString

func HandleChromeKeyString(key string, cfg OverlayConfig, st *LayerState, modal string, top, left, mw, mh, viewW, viewH int) ChromeKeyResult

HandleChromeKeyString is the key-string entry point (used by Bubble Tea v2 KeyPressMsg).

func HandleChromeKeyStringLayout

func HandleChromeKeyStringLayout(key string, cfg OverlayConfig, st *LayerState, layout ChromeLayout, viewW, viewH int) ChromeKeyResult

HandleChromeKeyStringLayout is the layout-cached key handler (Bubble Tea v2 KeyPressMsg).

type ChromeLayout

type ChromeLayout struct {
	Top, Left          int
	ModalW, ModalH     int
	ContentW, ContentH int
	Regions            ChromeRegions
}

ChromeLayout holds compositor origin, modal dimensions, content size, and hit regions for one paint frame. Pass a cached layout to HandleChromePointerLayout and HandleChromeKeyLayout to avoid re-measuring the modal string on every input event.

func LayoutChrome

func LayoutChrome(cfg OverlayConfig, st *LayerState, modal string, viewW, viewH int) ChromeLayout

LayoutChrome computes placement and hit regions from a rendered modal string.

func (ChromeLayout) CellInModal

func (l ChromeLayout) CellInModal(x, y int) bool

CellInModal reports whether terminal cell (x, y) lies inside the modal rectangle.

type ChromeMouseResult

type ChromeMouseResult struct {
	Consumed bool
	Pop      bool
	// MinimizeToggled is set when the press flipped the layer's Minimized
	// state. Stack callers use this to broadcast OverlayMinimizedMsg and
	// invoke OverlayMinimizer hooks; pure-chrome callers can ignore it
	// (Consumed is still true so they know to swallow the event).
	MinimizeToggled bool
}

ChromeMouseResult describes stack handling of a mouse message on window chrome.

func HandleChromeMouse

func HandleChromeMouse(msg tea.MouseMsg, cfg OverlayConfig, st *LayerState, modal string, top, left, mw, mh, viewW, viewH int) ChromeMouseResult

HandleChromeMouse updates drag state or requests pop for close. top/left/mw/mh must match painting.

func HandleChromeMouseLayout

func HandleChromeMouseLayout(msg tea.MouseMsg, cfg OverlayConfig, st *LayerState, layout ChromeLayout, viewW, viewH int) ChromeMouseResult

HandleChromeMouseLayout is like HandleChromeMouse but uses a precomputed ChromeLayout.

func HandleChromePointer

func HandleChromePointer(action ChromePointerAction, leftButton bool, x, y int, cfg OverlayConfig, st *LayerState, modal string, top, left, mw, mh, viewW, viewH int) ChromeMouseResult

HandleChromePointer is the Bubble Tea version-agnostic chrome handler (v2 maps into this).

func HandleChromePointerLayout

func HandleChromePointerLayout(action ChromePointerAction, leftButton bool, x, y int, cfg OverlayConfig, st *LayerState, layout ChromeLayout, viewW, viewH int) ChromeMouseResult

HandleChromePointerLayout is the layout-cached chrome pointer handler.

type ChromePointerAction

type ChromePointerAction uint8

ChromePointerAction describes a pointer event for window chrome handling.

const (
	ChromePointerPress ChromePointerAction = iota
	ChromePointerRelease
	ChromePointerMotion
)

type ChromeRegions

type ChromeRegions struct {
	TabTop, TabLeft, TabW, TabH                                int
	CloseX, CloseY, CloseW, CloseH                             int
	MinimizeX, MinimizeY, MinimizeW, MinimizeH                 int
	ResizeRightX, ResizeRightY, ResizeRightW, ResizeRightH     int
	ResizeBottomX, ResizeBottomY, ResizeBottomW, ResizeBottomH int
	ResizeCornerX, ResizeCornerY, ResizeCornerW, ResizeCornerH int
}

ChromeRegions describes tab, close, minimize, and resize hit areas relative to the modal's top-left cell.

func ComputeChromeRegions

func ComputeChromeRegions(wc WindowChrome, contentW, contentH int) ChromeRegions

ComputeChromeRegions returns hit rectangles for a framed modal's content size.

Region positions are based on the expanded tab layout. The minimize / restore buttons share a width (both glyphs are MinimizeButtonWidth), so the close button's column doesn't shift between expanded and minimized states — that keeps these regions valid as hit-test rects regardless of layer.Minimized. Resize regions are only populated when the modal is resizable AND has a body to drag against (contentH > 0); callers should additionally suppress resize dispatch when the layer is currently minimized.

type FocusTrap

type FocusTrap struct {
	Stack *OverlayStack
}

func (FocusTrap) InteractiveToBase

func (f FocusTrap) InteractiveToBase(msg tea.Msg) bool

type LayerState

type LayerState struct {
	OriginTop, OriginLeft       int
	OriginInitialized           bool
	Dragging                    bool
	DragOffsetX                 int
	DragOffsetY                 int
	ContentWidth, ContentHeight int
	ContentSizeInitialized      bool
	Resizing                    bool
	ResizeEdge                  ResizeEdge
	ResizeStartX, ResizeStartY  int
	ResizeStartW, ResizeStartH  int
	// Minimized collapses the window to its tab strip (no body, no
	// bottom border) when true. The window stays draggable in this
	// state, but resize handles disappear; the minimize button glyph
	// flips to RestoreButtonGlyph so clicking it expands the body
	// back to its previous ContentWidth / ContentHeight.
	Minimized bool
	// LastTabPressAt is the wall-clock time of the most recent left-
	// button press that landed in the tab's drag area. The chrome
	// handler uses it to detect a double-click (two presses within
	// DoubleClickThreshold) on the tab strip, which toggles the
	// minimized state. Exposed so tests can simulate "the first press
	// was N ms ago" without needing real sleeps or a clock injection.
	LastTabPressAt time.Time
}

LayerState holds per-entry origin and drag state for window chrome.

func (*LayerState) Reset

func (st *LayerState) Reset()

Reset clears all layer state (origin, drag, resize, content size).

func (*LayerState) ResetOrigin

func (st *LayerState) ResetOrigin()

ResetOrigin clears draggable origin so the next layout pass re-seeds from Placement.

type OverlayConfig

type OverlayConfig struct {
	Placement           Placement
	DimOpacity          float64
	CloseOnEscape       bool
	CloseOnClickOutside bool
	WindowChrome        WindowChrome
}

func DefaultOverlayConfig

func DefaultOverlayConfig() OverlayConfig

type OverlayMinimizedMsg

type OverlayMinimizedMsg struct {
	Minimized bool
}

OverlayMinimizedMsg is dispatched to a stack-pushed model when the user clicks the chrome's minimize / restore toggle and the layer's Minimized state flips. Models can react by pausing animations, freeing offscreen viewports, or showing a "(minimized)" hint elsewhere in the UI.

The library also continues to render the chrome's title bar while minimized — content delivered through OverlayTitler stays visible — so consumers don't have to do anything to keep the modal labeled.

type OverlayMinimizer

type OverlayMinimizer interface {
	OnOverlayMinimize(minimized bool) tea.Cmd
}

OverlayMinimizer is the optional interface a stack-pushed model can implement if it prefers a direct callback over an OverlayMinimizedMsg in its Update. The stack invokes OnOverlayMinimize before delivering the message; returning a tea.Cmd schedules follow-up work.

type OverlayOnCloser

type OverlayOnCloser interface {
	OnOverlayClose() tea.Cmd
}

type OverlayResizedMsg

type OverlayResizedMsg struct {
	NewContentWidth  int
	NewContentHeight int
}

OverlayResizedMsg is dispatched to a stack-pushed model when the user finishes a resize gesture through window chrome (mouse release on a resize edge, or an Alt+Shift arrow keypress). Models can react by adjusting viewports, re-wrapping content, etc.

NewContentWidth / NewContentHeight are the body dims the chrome will render at; they exclude the chrome's own border and tab.

type OverlayResizer

type OverlayResizer interface {
	OnOverlayResize(contentW, contentH int) tea.Cmd
}

OverlayResizer is the optional interface a stack-pushed model can implement if it prefers a direct callback over a OverlayResizedMsg in its Update. The stack invokes OnOverlayResize before delivering the OverlayResizedMsg; returning a tea.Cmd schedules follow-up work.

type OverlayStack

type OverlayStack struct {
	// contains filtered or unexported fields
}

func (*OverlayStack) Depth

func (s *OverlayStack) Depth() int

func (*OverlayStack) MainReceivesKeyMsg

func (s *OverlayStack) MainReceivesKeyMsg() bool

func (*OverlayStack) MainReceivesMouseMsg

func (s *OverlayStack) MainReceivesMouseMsg() bool

func (*OverlayStack) MouseTargetsTop

func (s *OverlayStack) MouseTargetsTop(msg tea.MouseMsg, viewW, viewH int) bool

MouseTargetsTop reports whether a mouse message should be routed to the top overlay entry. Hosts that want "modal stays open but background remains interactive" — e.g. a long-running progress modal where the user should still be able to browse the underlying view — call this before forwarding to OverlayStack.Update and route to their own main model instead when it returns false.

Returns true when any of:

  • There are no overlays (no top entry exists, so the host owns routing anyway — true here keeps the call safe to make unconditionally and consistent with MainReceivesMouseMsg).
  • A chrome gesture (drag / resize) is currently in progress on the top entry. The chrome state machine started inside the modal on a press, and the subsequent motion / release events must reach it even when the cursor has wandered outside the painted rect.
  • The coordinates fall inside the top entry's painted rectangle (the modal body plus any chrome the library draws around it — tab strip, resize handles, etc.).

Mouse wheel events at coordinates outside the modal return false so the host's main model can scroll the underlying view. Inside the modal they return true so the modal's own scroll target wins.

This is purely a hit-test query — it does not mutate stack state and is safe to call repeatedly per frame.

func (*OverlayStack) Pop

func (s *OverlayStack) Pop() (popped tea.Model, cmd tea.Cmd)

func (*OverlayStack) Push

func (s *OverlayStack) Push(m tea.Model, cfg OverlayConfig) tea.Cmd

func (*OverlayStack) SetTopLayerOrigin

func (s *OverlayStack) SetTopLayerOrigin(top, left int)

SetTopLayerOrigin sets the painted origin for the top entry when it uses draggable window chrome.

func (*OverlayStack) StackDepth

func (s *OverlayStack) StackDepth() int

func (*OverlayStack) Top

func (s *OverlayStack) Top() tea.Model

func (*OverlayStack) TopChromeLayout

func (s *OverlayStack) TopChromeLayout(viewW, viewH int) (top, left int, reg ChromeRegions, ok bool)

TopChromeLayout returns the compositor origin and chrome hit regions for the top stack entry.

func (*OverlayStack) Update

func (s *OverlayStack) Update(msg tea.Msg) tea.Cmd

func (*OverlayStack) View

func (s *OverlayStack) View(baseMain string, viewW, viewH int) string

type OverlayTitler

type OverlayTitler interface {
	OverlayTitle() string
}

OverlayTitler lets a stack-pushed model (or a Window content provider) supply a dynamic tab title. When a model implements this interface, the renderer reads OverlayTitle() each frame and uses that string in the WindowChrome tab instead of the static cfg.WindowChrome.Title. Returning "" falls back to the static title so the interface is safe to opt into even when you only sometimes have a dynamic title.

type Placement

type Placement struct {
	// contains filtered or unexported fields
}

func Center

func Center() Placement

func Fixed

func Fixed(top, left int) Placement

func RightDrawer

func RightDrawer() Placement

func (Placement) ClampedOrigin

func (p Placement) ClampedOrigin(modalW, modalH, viewW, viewH int) (top, left int)

ClampedOrigin returns Origin followed by the same overflow clamp as OverlayView. Use this when forwarding tea.MouseMsg or storing overlay bounds so geometry matches the compositor.

func (Placement) Origin

func (p Placement) Origin(modalW, modalH, viewW, viewH int) (top, left int)

Origin returns the placement anchor before OverlayView overflow clamping: negative top/left are pinned to 0, but if the modal is wider or taller than the viewport the origin is not shifted until compositing—use ClampedOrigin or ClampOverlayOrigin for coordinates that must match painting (e.g. mouse hit-testing).

type PlacementKind

type PlacementKind uint8

type ResizeEdge

type ResizeEdge uint8

ResizeEdge identifies which resize handle is active.

const (
	ResizeNone ResizeEdge = iota
	ResizeRight
	ResizeBottom
	ResizeCorner
)

type Window

type Window struct {
	State LayerState

	// OnResize is optional; WindowResizedMsg is always emitted on resize end.
	OnResize func(contentW, contentH int) tea.Cmd

	// Configure is an optional hook that lets the consumer override the
	// OverlayConfig the Window builds for each frame. It receives a fully
	// populated config (DefaultOverlayConfig + EnableWindowChrome + the
	// Window's resizable / keyboard / centerContent defaults already
	// applied) and may mutate any field — placement, dim opacity, close
	// behaviour, min size, the chrome mask rune, etc. WindowChrome.Enabled
	// must remain true; if Configure clears it, the Window forces it back
	// on so the chrome path stays consistent.
	//
	// Use this when you need a chromed modal but want different defaults
	// than configFor's "centered, MinWidth=32, MinHeight=6, U+E000 mask".
	Configure func(*OverlayConfig)
	// contains filtered or unexported fields
}

Window composes one chromed modal per frame for state-machine-driven apps (no OverlayStack). Pass a stable non-empty key per modal slot; a key change from the previous frame resets State so reopens recenter. An empty key clears State when no modal is open.

func (*Window) LastChromeLayout

func (w *Window) LastChromeLayout() (ChromeLayout, bool)

LastChromeLayout returns layout from the most recent View or Update on this frame.

func (*Window) Update

func (w *Window) Update(msg tea.Msg, content, title, key string, viewW, viewH int, closeCmd tea.Cmd) (consumed bool, cmd tea.Cmd)

Update routes input for the active chromed modal. closeCmd is returned when the user clicks [x] or when CloseOnEscape is enabled and Esc is pressed.

func (*Window) View

func (w *Window) View(base, content, title, key string, viewW, viewH int) string

View wraps content in window chrome and composites it over base. No-op when key is empty, content is empty, or the viewport is zero-sized.

type WindowChrome

type WindowChrome struct {
	Enabled            bool
	Title              string
	ShowCloseButton    bool
	ShowMinimizeButton bool // render [-] / [+] toggle to the left of the close button
	AutoWrap           bool
	TitleBarHeight     int // legacy; tab layout uses TabOffsetTop + tab rows when zero
	Draggable          bool
	TabBackground      string // lipgloss color for tab fill (default muted "238")
	TabForeground      string // lipgloss color for tab text
	TabBorder          string // lipgloss color for tab border runes
	TabOffsetTop       int    // rows above tab (default 1)
	TabOffsetLeft      int    // columns left of tab (default 0)
	ChromeMaskRune     rune   // pass-through padding in auto-wrap chrome; 0 uses DefaultChromeMaskRune
	Resizable          bool   // drag right/bottom edges (and corner) to resize content
	Keyboard           bool   // Alt+arrow move, Alt+Shift+arrow resize
	KeyStep            int    // cells per keypress when Keyboard is enabled (default 1)
	CenterContent      bool   // keep content centered in the body when Resizable
	ContentPadTop      int    // blank lines pinned to the top of the resizable body
	MinWidth           int    // minimum content width when Resizable
	MinHeight          int    // minimum content height (lines) when Resizable
}

WindowChrome configures optional tab title bar, drag, and close for a stack entry. When Enabled is true, use EnableWindowChrome for recommended defaults, or set AutoWrap, Draggable, and ShowCloseButton explicitly.

func EnableWindowChrome

func EnableWindowChrome(title string) WindowChrome

EnableWindowChrome returns a WindowChrome with tab title bar, drag, close, and auto-wrap enabled.

func (WindowChrome) Effective

func (w WindowChrome) Effective() WindowChrome

Effective returns chrome settings with defaults applied when Enabled.

type WindowFrameOpts

type WindowFrameOpts struct {
	Width           int
	TitleStyle      lipgloss.Style
	TabBackground   string
	TabForeground   string
	TabBorder       string
	TabOffsetTop    int
	TabOffsetLeft   int
	ShowCloseButton bool
}

WindowFrameOpts styles the tab chrome rendered by WindowFrame.

type WindowResizedMsg

type WindowResizedMsg struct {
	NewContentWidth  int
	NewContentHeight int
}

WindowResizedMsg is sent when a resizable window finishes a resize (mouse release or keyboard grow).

Directories

Path Synopsis
examples
colors command
confirm command
draggable command
form command
simple command
spinner command
stack command
transparency command
v2simple command
internal
Package overlayv2 provides OverlayStack for Bubble Tea v2 (charm.land/bubbletea/v2).
Package overlayv2 provides OverlayStack for Bubble Tea v2 (charm.land/bubbletea/v2).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL