Frames, Borders, Tiles
The drawing types define how frames, borders, filled areas, and character combinations are rendered into writable buffers.
They form the foundation for panels, boxes, separators, decorative borders, tiled backgrounds, and line-art style layouts.
Details about the example output on this page
The examples below were rendered with the dedicated documentation helper
doc/tools/drawing-reference.cpp at a fixed width of
70 terminal columns. This makes it easy to regenerate the visual output
together with the code snippets.
Usage
Filling Panels and Drawing Simple Frames
WritableBuffer provides convenient functions to fill rectangles and draw frames.
The drawing types on this page determine how those operations appear.
const auto panel = Rectangle{2, 2, 30, 10};
buffer.fill(panel, Block{" ", Color{fg::Inherited, bg::Blue}});
buffer.drawFrame(panel, FrameStyle::LightWithRoundedCorners, Color{fg::BrightWhite, bg::Blue});
buffer.drawBlockText("Overview", panel.insetBy(Margins{1}), Alignment::TopLeft);
FrameStyle is the fastest way to choose one of the built-in Unicode box styles.
It works well for common UI elements such as panels, dialogs, and separators.
You can restyle the same layout code simply by changing the enum value:
buffer.drawFilledFrame(
Rectangle{2, 4, 20, 4},
FrameStyle::Light,
Block{" ", Color{fg::Inherited, bg::Blue}},
Color{fg::BrightCyan, bg::Inherited});
buffer.drawFilledFrame(
Rectangle{25, 4, 20, 4},
FrameStyle::LightWithRoundedCorners,
Block{" ", Color{fg::Inherited, bg::Green}},
Color{fg::BrightGreen, bg::Inherited});
buffer.drawFilledFrame(
Rectangle{48, 9, 20, 4},
FrameStyle::FullBlock,
Block{" ", Color{fg::Inherited, bg::BrightBlack}});
Built-in FrameStyle presets
Choose a built-in style without changing your layout code.
Light Rounded Heavy
┌──────────────────┐ ╭──────────────────╮ ┏━━━━━━━━━━━━━━━━━━┓
│ panel │ │ panel │ ┃ panel ┃
│ │ │ │ ┃ ┃
└──────────────────┘ ╰──────────────────╯ ┗━━━━━━━━━━━━━━━━━━┛
Double OuterHalf FullBlock
╔══════════════════╗ ▛▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▜ ████████████████████
║ panel ║ ▌ panel ▐ █ panel █
║ ║ ▌ ▐ █ █
╚══════════════════╝ ▙▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▟ ████████████████████
Combining Frames Automatically
When multiple frames intersect, drawing them directly on top of each other would produce broken or overlapping characters.
BlockCombinationStyle resolves these overlaps by choosing the correct combined glyph for each intersection.
auto frameStyle = Block16Style::lightFrame();
auto combination = BlockCombinationStyle::commonBoxFrame();
buffer.drawFrame(Rectangle{2, 2, 18, 8}, frameStyle, combination, Color{fg::BrightCyan, bg::Black});
buffer.drawFrame(Rectangle{8, 5, 20, 8}, frameStyle, combination, Color{fg::BrightYellow, bg::Black});
This ensures that intersecting lines produce consistent box-drawing characters instead of visual artifacts.
Drawing Grid Layouts
Use GridLayout when you want a stable table-like layout where content cells keep their own rectangles and the frame
lines are drawn around them.
The FrameBorder object describes which outer and inner line groups are visible.
A None element consumes no terminal cells, while supported line styles consume one cell and are resolved with their
neighbors automatically.
Block frame styles are not grid line styles and are treated like None here.
auto border = FrameBorder{FrameStyle::Light, Color{fg::BrightWhite, bg::Black}};
border.set(FrameBorderElement::HLine, FrameStyle::Heavy, Color{fg::BrightWhite, bg::Black});
auto layout = GridLayout{{16, 16, 16}, {3, 3}};
auto origin = Position{2, 2};
for (std::size_t row = 0; row < layout.rowCount(); ++row) {
for (std::size_t column = 0; column < layout.columnCount(); ++column) {
buffer.drawBlockText(
"cell",
layout.cellRect(row, column, origin, border),
Alignment::Center,
Color{fg::BrightCyan, bg::Black});
}
}
buffer.drawGridLayout(origin, layout, border);
This keeps content placement independent from the selected border style.
If you later hide separator lines or switch from light to double borders, the same cellRect() calls still describe
the usable content area.
The difference is easiest to see when the same pair of frames is rendered once with plain overwrite behavior and once with a box-aware combiner:
auto style = Block16Style::lightFrame();
buffer.drawFrame(
Rectangle{2, 2, 17, 6},
style,
BlockCombinationStyle::overwrite(),
Color{fg::BrightCyan, bg::Inherited});
buffer.drawFrame(
Rectangle{8, 4, 18, 6},
style,
BlockCombinationStyle::overwrite(),
Color{fg::BrightYellow, bg::Inherited});
buffer.drawFrame(
Rectangle{37, 2, 17, 6},
style,
BlockCombinationStyle::commonBoxFrame(),
Color{fg::BrightCyan, bg::Inherited});
buffer.drawFrame(
Rectangle{43, 4, 18, 6},
style,
BlockCombinationStyle::commonBoxFrame(),
Color{fg::BrightYellow, bg::Inherited});
overwrite() commonBoxFrame()
Later frames simply replace earlier Intersections become matching
┌───────────────┐ ┌───────────────┐
│ │ │ │
│ ┌────────────────┐ │ ┌─────────┼──────┐
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
└─────│─────────┘ │ └─────┼─────────┘ │
│ │ │ │
└────────────────┘ └────────────────┘
To define your own combination behavior, use SimpleBlockCombinationStyle for straightforward mappings or
MatrixCombinationStyle for full control over all combinations.
Repeating 9-Tile Styles
Tile9Style fills or frames a rectangle using a repeating 3×3 tile layout.
This lets you define separate characters for corners, edges, and the center while still scaling cleanly to any size.
const auto panel = Rectangle{2, 2, 30, 10};
const auto style = Tile9Style::create("╔═╗║ ║╚═╝");
buffer.fill(panel, style, Color{fg::BrightBlack, bg::Black});
buffer.drawFrame(panel, style, Color{fg::BrightCyan, bg::Black});
The 9-tile layout covers the standard case: corners, edges, and center.
You can also read a configured tile directly by name, for example style->block(Tile9Style::Element::West) or
style->block(Tile9Style::Element::Center).
This is useful for small theme-controlled text fragments that reuse the same tile table without drawing a rectangle.
If you also need specialized tiles for degenerate cases such as a single row, a single column, or a single cell, construct the style with 16 characters instead.
The left example below uses a decorative 9-tile pattern. The right side uses letter-coded 16-tile input so the special degenerate tiles are easy to identify:
const auto decorative = Tile9Style::create("╔═╗║·║╚═╝");
buffer.fill(Rectangle{3, 2, 28, 7}, decorative, Color{fg::BrightCyan, bg::Inherited});
const auto degenerate = Tile9Style::create("ABCDEFGHIJKLMNOP");
buffer.fill(Rectangle{39, 2, 10, 5}, degenerate, Color{fg::BrightYellow, bg::Inherited});
buffer.fill(Rectangle{52, 2, 10, 1}, degenerate, Color{fg::BrightYellow, bg::Inherited});
buffer.fill(Rectangle{52, 4, 1, 5}, degenerate, Color{fg::BrightYellow, bg::Inherited});
buffer.fill(Rectangle{57, 4, 1, 1}, degenerate, Color{fg::BrightYellow, bg::Inherited});
9 tiles repeat edges and center 16 tiles cover thin edge cases
row
╔══════════════════════════╗ ABBBBBBBBC JKKKKKKKKL
║··························║ DEEEEEEEEF
║··········Repeat··········║ DEEEEEEEEF M P cell
║··························║ DEEEEEEEEF N
║··························║ GHHHHHHHHI N
║··························║ N
╚══════════════════════════╝ normal O
column
Configuring Reusable Frame Presets
When a frame configuration goes beyond a simple style and color, FrameDrawOptions lets you bundle all settings into
a reusable object.
auto panelStyle = FrameDrawOptions{};
panelStyle.setStyle(FrameStyle::LightWithRoundedCorners);
panelStyle.setFillBlock(Block{" ", Color{fg::Inherited, bg::Black}});
panelStyle.setFrameColorSequence(
ColorSequence{
Color{fg::BrightBlue, bg::Black},
Color{fg::BrightCyan, bg::Black},
Color{fg::BrightWhite, bg::Black},
},
FrameColorMode::ChasingBorderCW);
panelStyle.setFillColor(Color{fg::Inherited, bg::Blue});
buffer.drawFrame(Rectangle{2, 2, 28, 10}, panelStyle, animationCycle);
buffer.drawFrame(Rectangle{34, 2, 28, 10}, panelStyle, animationCycle + 4);
This approach is especially useful when the same panel style is reused throughout your application, or when you work with animated borders, custom fill behavior, or non-default combination rules.
Because FrameDrawOptions bundles style, fill, and color animation in one object, it is also a good fit for reusable
presets:
auto stripeOptions = FrameDrawOptions{};
stripeOptions.setStyle(FrameStyle::Double);
stripeOptions.setFillBlock(Block{" "});
stripeOptions.setFrameColorSequence(
ColorSequence{
Color{fg::BrightYellow, bg::Inherited},
Color{fg::BrightMagenta, bg::Inherited},
Color{fg::BrightCyan, bg::Inherited},
},
FrameColorMode::VerticalStripes);
stripeOptions.setFillColorSequence(
ColorSequence{
Color{fg::Inherited, bg::Blue},
Color{fg::Inherited, bg::Magenta},
},
FrameColorMode::HorizontalStripes);
auto chasingOptions = FrameDrawOptions{};
chasingOptions.setStyle(FrameStyle::Heavy);
chasingOptions.setFillBlock(Block{"·", Color{fg::BrightBlack, bg::Inherited}});
chasingOptions.setFrameColorSequence(
ColorSequence{
Color{fg::BrightRed, bg::Inherited},
Color{fg::BrightYellow, bg::Inherited},
Color{fg::BrightGreen, bg::Inherited},
Color{fg::BrightCyan, bg::Inherited},
},
FrameColorMode::ChasingBorderCW);
buffer.drawFrame(Rectangle{24, 1, 22, 8}, stripeOptions);
buffer.drawFrame(Rectangle{47, 1, 22, 8}, chasingOptions, 3);
Static fill Striped sequences Chasing border
╭────────────────────╮ ╔════════════════════╗ ┏━━━━━━━━━━━━━━━━━━━━┓
│ │ ║ ║ ┃····················┃
│ │ ║ ║ ┃····················┃
│ │ ║ ║ ┃····················┃
│ │ ║ ║ ┃····················┃
│ │ ║ ║ ┃····················┃
│ │ ║ ║ ┃····················┃
╰────────────────────╯ ╚════════════════════╝ ┗━━━━━━━━━━━━━━━━━━━━┛
fixed colors stripe modes cycle = 3
Choosing the Right Style
Each drawing style has a slightly different purpose:
Use
FrameStylewhen a predefined border style is enough.Use
Block16Stylewhen each cell should adapt to its neighbors, for example for custom line art or circuit-like rendering.Use
Tile9Stylewhen corners, edges, and center tiles should scale independently.
Choosing the right abstraction early keeps your rendering code simpler and avoids unnecessary post-processing later.