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 an AlignedSource with 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.