Geometry
The geometry classes provide the building blocks for positioning and layout inside a terminal buffer. They describe sizes, positions, rectangles, and directions, and let you derive new regions from existing ones.
Using explicit geometry types keeps layout code readable and makes it easier to build structured terminal interfaces.
Details about the example output on this page
The examples below were rendered with the dedicated documentation
helper doc/tools/geometry-reference.cpp at a fixed width of
72 terminal columns. This makes it easy to regenerate the visual
output together with the code snippets.
Usage
Deriving Layout Regions from a Canvas
The geometry types are designed to make screen layout explicit and easy to follow. Instead of calculating coordinates manually, you derive smaller regions from a larger canvas and keep each intermediate rectangle named.
const auto canvas = Rectangle{2, 3, 68, 8};
const auto header = canvas.subRectangle(Anchor::TopCenter, Size{0, 2}, Margins{0, 1, 0, 1});
const auto footer = canvas.subRectangle(Anchor::BottomCenter, Size{0, 1}, Margins{0, 1, 0, 1});
const auto body = canvas.insetBy(Margins{2, 1, 1, 1});
const auto sidebar = body.subRectangle(Anchor::Left, Size{18, 0}, Margins{0, 1, 0, 0});
const auto content = body.subRectangle(Anchor::Right, Size{body.width() - 19, 0}, Margins{0});
buffer.drawFrame(canvas, FrameStyle::Double, Color{fg::BrightWhite, bg::Inherited});
buffer.drawBlockText("Header", header, Alignment::Center, Color{fg::BrightWhite, bg::Blue});
buffer.drawBlockText("Sidebar", sidebar, Alignment::Center, Color{fg::BrightWhite, bg::Green});
buffer.drawBlockText("Content", content, Alignment::Center, Color{fg::BrightWhite, bg::Magenta});
buffer.drawBlockText("Footer", footer, Alignment::Center, Color{fg::BrightWhite, bg::BrightBlack});
Passing 0 as the width or height to Rectangle::subRectangle() means “use the full available size on that
axis”.
This is especially useful for headers, footers, and sidebars that should stretch with the parent rectangle.
subRectangle() + insetBy() keep layouts explicit
One canvas can derive headers, sidebars, content, and footers cleanly.
╔╤╤╤╤╤╤╤╤╤╤╤╤╤╤╤╤╤╤╤╤╤╤╤╤╤╤╤╤╤╤header╤╤╤╤╤╤╤╤╤╤╤╤╤╤╤╤╤╤╤╤╤╤╤╤╤╤╤╤╤╤╗
║╰┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴╯║
║╭────────────────╮ ╭─────────────────────────────────────────────╮║
║│ │ │ │║
║│ sidebar │ │ content │║
║│ │ │ │║
║╰────────────────╯ ╰─────────────────────────────────────────────╯║
╚══════════════════════════════footer══════════════════════════════╝
Combining, Intersecting, and Testing Rectangles
Rectangle supports union and intersection directly.
This is useful when you need to compute redraw regions, selection overlaps, or the visible area shared by two panels.
const auto a = Rectangle{4, 2, 13, 5};
const auto b = Rectangle{11, 4, 16, 5};
if (a.overlaps(b)) {
const auto dirtyRegion = a | b;
const auto sharedRegion = a & b;
buffer.drawFrame(dirtyRegion, FrameStyle::Heavy, Color{fg::BrightMagenta, bg::Inherited});
buffer.fill(sharedRegion, Block{" ", Color{fg::Inherited, bg::BrightBlack}});
buffer.drawFrame(sharedRegion, FrameStyle::Double, Color{fg::BrightGreen, bg::Inherited});
}
Use a | b when you need the combined area covered by both rectangles.
Use a & b when you need only their shared visible area.
The inexpensive Rectangle::overlaps() check is often the right guard before doing extra work.
A | B A & B
┏━━━━━━━━━━━┯━━━━━━━━━┓ ┌───────────┐
┃ A │ ┃ │ A │
┃ ┌────┼─────────┨ │ ╔════╗─────────┐
┃ │ │ B ┃ │ ║ ║ B │
┠──────┼────┘ ┃ └──────╚════╝ │
┃ │ ┃ │ │
┗━━━━━━┷━━━━━━━━━━━━━━┛ └──────────────┘
union intersection
Aligning Smaller Content and Cropping Larger Sources
There are two closely related alignment tools:
Rectangle::alignmentOffset()computes where a smaller block should start.Rectangle::alignedSource()returns anAlignedSourcewith both the effective target and the cropped source rectangle.
const auto badgeRect = Rectangle{
panel.alignmentOffset(Size{8, 3}, Alignment::BottomRight),
Size{8, 3}};
buffer.drawFrame(badgeRect, FrameStyle::Double, Color{fg::BrightCyan, bg::Inherited});
const auto aligned = panel.insetBy(Margins{1}).alignedSource(
Rectangle{0, 0, 18, 5},
Alignment::Center);
aligned.targetRect.forEach([&](const Position pos) {
const auto sourceX = aligned.sourceRect.x1() + (pos.x() - aligned.targetRect.x1());
const auto sourceY = aligned.sourceRect.y1() + (pos.y() - aligned.targetRect.y1());
// Sample from the centered source area here.
});
This saves you from writing separate “place when smaller” and “crop when larger” branches. The same alignment value handles both cases.
alignmentOffset() places smaller content; alignedSource() crops larger
╔══════╦───────────┐ ┌──────────────────┐ ┌──────────────────┐
║ 8x3 ║ │ │ ╔══════╗ │ │ │
╞══════╝ │ │ ║ 8x3 ║ │ │ ╔══════╡
│ │ │ ╚══════╝ │ │ ║ 8x3 ║
└──────────────────┘ └──────────────────┘ └───────────╩══════╝
TopLeft Center BottomRight
╔══════════════════════════════════════╗
║ 234567890123456789 ║
║ 456789012345678901 ║
║ 678901234567890123 ║
╚══════════════════════════════════════╝
center crop of 18x5 source
Splitting a Rectangle into Grid Cells
Rectangle::gridCells() divides a larger canvas into evenly spaced sub-rectangles.
This is useful for dashboards, menu grids, option panels, and any row-major layout where each cell should stay
predictable.
const auto grid = Rectangle{2, 2, 68, 8};
const auto cells = grid.gridCells(2, 3, 2, 1);
for (std::size_t index = 0; index < cells.size(); ++index) {
const auto &cell = cells[index];
buffer.drawFrame(cell, FrameStyle::Light, Color{fg::BrightWhite, bg::Inherited});
buffer.drawBlockText(
el::StringFormat{"#{} {}x{}"_el}.build(index, cell.width(), cell.height()),
cell,
Alignment::Center);
}
Remainder pixels are distributed to the top-left cells first, and the resulting vector is returned in row-major order from left to right, then top to bottom.
If the requested number of rows, columns, and spacing no longer fits into the rectangle, gridCells() throws
std::invalid_argument.
gridCells() distributes remainder to the top-left cells and keeps
┌────────────────────┐ ┌───────────────────┐ ┌───────────────────┐
│ #0 22x4 │ │ #1 21x4 │ │ #2 21x4 │
│ │ │ │ │ │
└────────────────────┘ └───────────────────┘ └───────────────────┘
┌────────────────────┐ ┌───────────────────┐ ┌───────────────────┐
│ #3 22x3 │ │ #4 21x3 │ │ #5 21x3 │
└────────────────────┘ └───────────────────┘ └───────────────────┘
Walking Neighbors and Frame Perimeters
Position and Rectangle include traversal helpers that are useful for custom layout logic, collision
checks, and procedural drawing.
const auto center = Position{12, 5};
for (const auto pos : center.cardinalFour()) {
buffer.set(pos, Block{U'+', Color{fg::BrightYellow, bg::Inherited}});
}
for (const auto pos : center.ringEight()) {
// ringEight() returns the eight surrounding positions clockwise.
}
const auto frame = Rectangle{46, 2, 18, 7};
frame.forEachInFrame([&](const Position pos, const int index) {
buffer.set(
pos,
Block{static_cast<char32_t>(U'0' + (index % 10)), Color{fg::BrightWhite, bg::Inherited}});
});
const auto currentIndex = frame.frameIndex(Position{63, 5});
The Direction wrapper fits into the same workflow when directions come from configuration or input, because it
can convert to and from deltas and strings.
auto cursor = Position{10, 5};
cursor += Direction::fromString("east").toDelta();
Position::ringEight() Rectangle::forEachInFrame()
012345678901234567
5 8
5+7 4 9
+X+ 3 0
3+1 2 1
1 2
098765432109876543
0..7 clockwise around X index order around frame
Deriving Bounds and Keeping Cursors in Range
Two other helpers are worth using regularly:
Rectangle::bounds()creates the smallest rectangle that contains a set of positions.Rectangle::clamp()keeps a cursor or probe position inside a rectangle.
const auto points = PositionList{
Position{5, 5},
Position{9, 3},
Position{13, 6},
Position{17, 4},
Position{11, 7},
};
const auto highlight = Rectangle::bounds(points).expandedBy(Margins{1});
const auto viewport = Rectangle{46, 2, 16, 6};
const auto rawCursor = Position{66, 8};
const auto safeCursor = viewport.clamp(rawCursor);
if (viewport.contains(safeCursor)) {
buffer.set(safeCursor, Block{"@", Color{fg::BrightGreen, bg::Inherited}});
}
This pattern works well for drag selections, hit testing, and cursor movement that should stop cleanly at the viewport boundary instead of spilling outside the drawable area.
Rectangle::bounds() Rectangle::clamp()
╔═════════════╗ ┌───viewport───┐
║ • ║ │ │
║ •║ │ │
║• ║ │ │
║ • ║ │ │
║ • ║ └──────────────@
╚═════════════╝ raw X
derive one enclosing rectangle clamped to nearest edge
Orientation Helpers and Coordinate Values
Orientation and Coordinate are the small glue types that keep geometry code readable when logic needs to
switch between horizontal and vertical behavior.
auto axis = Orientation::Horizontal;
auto size = Size{24, 7};
auto cursor = Position{4, 2};
const auto primaryExtent = size.component(axis);
const auto primaryOffset = cursor.component(axis);
const auto crossAxis = axis.crossed();
if (crossAxis == Orientation::Vertical) {
buffer.drawBlockText(
el::StringFormat{"axis={} offset={}"_el}.build(primaryExtent, primaryOffset),
Rectangle{2, 2, 24, 1},
Alignment::Left);
}
The coordinate() helpers on Size and Position let one code path serve both axes without branching
on x/y or width/height names all the way through the implementation.