Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Lines

Drawing lines is a common operation when building a UI. In AppCUI lines can be drawn in several ways:

  • use special characters (single, double, thick, ASCII, rounded or Braille) for axis-aligned or free-form lines
  • use a generic character to fill a line
  • connect two points with an orthogonal (elbow) path
  • connect a list of points as a polyline, with optional caps and joints

Using special characters to draw lines

The following methods can be used to draw lines on a surface using special characters:

MethodDescription
draw_horizontal_line(...)Draws a horizontal line on the surface. The line will be drawn from left to right.
draw_vertical_line(...)Draws a vertical line on the surface. The line will be drawn from top to bottom.
draw_horizontal_line_with_size(...)Draws a horizontal line on the surface with a specific length. The line will be drawn from left to right, starting from a given point and a width.
draw_vertical_line_with_size(...)Draws a vertical line on the surface with a specific length. The line will be drawn from top to bottom, starting from a given point and a width.
draw_line(...)Draws a straight line between two points using a specific line type and character attributes (Bresenham for diagonals)
draw_orthogonal_line(...)Connects two points with horizontal and vertical segments only (never a diagonal), using the corners from the given line type
draw_polyline(...)Connects a list of points using a PolyLineFormat (style, colors, optional caps and joints)

These methods take a parameter line_type (LineType) that specifies the visual style of the line. The line type can be one of the following values:

ValueCharacters being used
Single─, │, ┌, ┐, └, ┘, ├, ┤, ┬, ┴, ┼
Double═, ║, ╔, ╗, ╚, ╝, ╠, ╣, ╦, ╩, ╬
SingleThick━, ┃, ┏, ┓, ┗, ┛, ┣, ┫, ┳, ┻, ╋
Border▄, ▀, █
Ascii|, -, +
AsciiRound|, -, +, \\ , \/
SingleRound╭, ╮, ╯, ╰, ─, │
BrailleBraille characters

Example:

#![allow(unused)]
fn main() {
use appcui::graphics::{Surface, LineType, CharAttribute, Color};

let mut surface = Surface::new(100, 50);
surface.draw_vertical_line(10, 10, 20, 
                            LineType::Single, 
                            CharAttribute::with_color(Color::White, Color::Black));
}

draw_line works for horizontal, vertical and diagonal segments. For Single, Double, SingleThick, SingleRound and Border it uses a Bresenham path and picks matching box-drawing glyphs. Ascii and AsciiRound use ASCII approximations; Braille uses Braille dots.

Orthogonal lines

draw_orthogonal_line never draws a diagonal. The two points are connected with horizontal and vertical pieces, and corners are taken from LineType. If the points already share an X or Y coordinate, a single straight segment is drawn. If they are the same point, nothing is drawn.

The OrthogonalDirection parameter chooses how the path bends:

ValuePath
HorizontalFirstHorizontal to the destination column, then vertical
VerticalFirstVertical to the destination row, then horizontal
HorizontalUntilMiddleHorizontal to the midpoint, then vertical, then horizontal
VerticalUntilMiddleVertical to the midpoint, then horizontal, then vertical
AutoHorizontalFirst when the run is wider than it is tall, otherwise VerticalFirst
HorizontalFirst              VerticalFirst

(x1,y1) ────────┐            (x1,y1)
                │               │
                (x2,y2)         └──────── (x2,y2)

HorizontalUntilMiddle        VerticalUntilMiddle

(x1,y1) ──┐                  (x1,y1)
          │                     │
          └── (x2,y2)      ┌────┘
                           │
                           (x2,y2)

Example:

#![allow(unused)]
fn main() {
use appcui::prelude::*;

let mut surface = Surface::new(40, 12);
surface.draw_orthogonal_line(
    2, 2, 20, 8,
    LineType::Single,
    OrthogonalDirection::HorizontalFirst,
    charattr!("white,black"),
);
}

Polylines

draw_polyline connects a list of Points in order. Consecutive points are drawn with draw_line. Orthogonal turns (all eight incoming-to-outgoing direction pairs) get a matching corner glyph from LineType. Nothing is drawn if the slice has fewer than two points.

If the first and last points are the same, the path is treated as closed: start and end caps are skipped and the closing corner is written as well.

Open U (caps allowed)          Closed (first point repeated)

│        │                     ┌────────┐
│        │                     │        │
│        │                     │        │
└────────┘                     └────────┘

A path with several inner vertices gets a corner at every turn (here: └, ┐, └, ┘):

Automatic corners

│         │
│         │
└────┐    │
     │    │
     └────┘

The eight orthogonal turns resolve as follows (incoming direction → outgoing direction):

Right → Up          Down → Left         Up → Left           Right → Down

    │                   │                   ──┐                 ──┐
  ──┘                 ──┘                     │                   │


Left → Down         Up → Right          Down → Right        Left → Up

  ┌──                 ┌──                   │                   │
  │                   │                     └──                 └──

The look of the polyline is described by PolyLineFormat. Build one with PolyLineFormatBuilder:

Builder methodDescription
new(line_type, attr)Starts a builder with a line style and the attributes used for every segment
start_cap(...)Glyph at the first point (LineCap). Ignored on a closed path
start_attr(...)Attributes of the start cap. If omitted, the line attributes are used
end_cap(...)Glyph at the last point (LineCap). Ignored on a closed path
end_attr(...)Attributes of the end cap. If omitted, the line attributes are used
joint(...)A custom character at every inner vertex, instead of an automatically chosen corner
joint_attr(...)Attributes of the joints (automatic corners or a custom joint). If omitted, the line attributes are used
build()Builds the PolyLineFormat value

By default the format has no start cap, no end cap, and no custom joint. Orthogonal corners are then chosen automatically from the incoming and outgoing segment directions.

A custom joint(...) character replaces every inner vertex (the start and end of an open path stay as line endpoints or caps):

Automatic corners              Custom joint ('•')

│         │                    │         │
│         │                    │         │
└────┐    │                    •────•    │
     │    │                         │    │
     └────┘                         •────•

Line caps

LineCap is the glyph drawn at the start or end of an open polyline. Caps are applied after the segments are drawn.

ValueBehavior
AutoMerges the endpoint with neighboring box-drawing characters (a T-junction or similar). Intended for connectors that attach to a rectangle or another line
ArrowA directional arrow (↑, ↓, ←, →) inferred from the first or last segment
TriangleA directional triangle (▲, ▼, ◀, ▶) inferred from the first or last segment
Char(c)The same custom character at the endpoint, regardless of direction

Caps are inferred from the first segment (start) and the last segment (end). On an open U that goes down, across, then up:

No caps            Arrow               Triangle            Char('S') / Char('E')
                                                          + joint '*'

│       │          ↓       ↑           ▼       ▲           S       E
│       │          │       │           │       │           │       │
│       │          │       │           │       │           │       │
└───────┘          └───────┘           └───────┘           *───────*

LineCap::Auto is different: it does not draw an arrow. It calls write_box_junction at each end so the connector merges into whatever box-drawing characters are already there (├ / ┤ below):

┌──────────┐
│          │
│          ├─────┐
│          │     │
│          │     │
└──────────┘     │
                 │      ┌──────────┐
                 └──────┤          │
                        │          │
                        └──────────┘

The first snippet below (Arrow start + Triangle end) looks like this:

↓       ▲
│       │
│       │
└───────┘

Example:

#![allow(unused)]
fn main() {
use appcui::prelude::*;

let mut surface = Surface::new(40, 12);

// Open orthogonal U with arrow and triangle caps
let format = PolyLineFormatBuilder::new(LineType::Single, charattr!("white,black"))
    .start_cap(LineCap::Arrow)
    .end_cap(LineCap::Triangle)
    .build();
surface.draw_polyline(
    &[
        Point::new(2, 1),
        Point::new(2, 6),
        Point::new(10, 6),
        Point::new(10, 1),
    ],
    &format,
);

// Closed rectangle (first point repeated at the end)
let rect = PolyLineFormatBuilder::new(LineType::Double, charattr!("aqua,black")).build();
surface.draw_polyline(
    &[
        Point::new(14, 1),
        Point::new(14, 6),
        Point::new(24, 6),
        Point::new(24, 1),
        Point::new(14, 1),
    ],
    &rect,
);

// Connector that merges into existing boxes at both ends
let connector = PolyLineFormatBuilder::new(LineType::Single, charattr!("yellow,black"))
    .start_cap(LineCap::Auto)
    .end_cap(LineCap::Auto)
    .build();
surface.draw_polyline(
    &[Point::new(10, 4), Point::new(14, 4)],
    &connector,
);
}

A PolyLineFormat can also be edited after it is built (set_attr, set_line_type, set_start_cap, set_end_cap, set_joint, and the matching *_attr setters). Pass None to a cap or joint setter to clear it.

Box junctions

write_box_junction(x, y) rewrites the character at (x, y) as a box-drawing junction based on its four neighbors (left, above, right, below). If they form a recognized junction (T-split, cross, corner, and so on), the cell is replaced with that glyph. Colors and flags are left unchanged. If no junction matches, the cell is not modified.

This is useful after drawing overlapping lines, or after attaching a polyline with LineCap::Auto. Coordinates are relative to the current origin. The method also accepts a cell one character outside the clip rectangle so junctions on a border remain reachable.

A single crossing, before and after write_box_junction at the intersection:

Drawn lines (no junction)      After write_box_junction

─────│─────                    ─────┬─────
     │                              │
     │                              │

Several crossings on the same surface (single and double lines, plus the outer rectangle) produce T-junctions, mixed joints and a cross at every meeting point:

┌───┬───────────────────╥─────┐
│   │                   ║     │
│   │                   ║     │
│   │                   ║     │
├───┼───────────────────╫─────┤
│   │                   ║     │
│   │                   ║     │
╞═══╪═══════════════════╬═════╡
│   │                   ║     │
└───┴───────────────────╨─────┘

Call write_box_junction once for each meeting point (the inner crosses, the T-junctions on the border, and the mixed single/double joints).

#![allow(unused)]
fn main() {
use appcui::prelude::*;

let mut surface = Surface::new(40, 10);
let attr = charattr!("white,black");
surface.draw_line(0, 4, 30, 4, LineType::Single, attr);
surface.draw_line(0, 7, 30, 7, LineType::Double, attr);
surface.draw_line(4, 0, 4, 10, LineType::Single, attr);
surface.draw_line(24, 0, 24, 10, LineType::Double, attr);
surface.draw_rect(Rect::new(0, 0, 30, 9), LineType::Single, attr);

for (x, y) in [
    (4, 4), (24, 4), (4, 7), (24, 7),
    (0, 4), (30, 4), (4, 0), (24, 0),
    (4, 9), (24, 9), (0, 7), (30, 7),
] {
    surface.write_box_junction(x, y);
}
}

Using a generic character to draw lines

The following methods can be used to draw lines on a surface using a generic character:

MethodDescription
fill_horizontal_line(...)Fills a horizontal line on the surface. The line will be filled from left to right with a provided Character
fill_vertical_line(...)Fills a vertical line on the surface. The line will be filled from top to bottom with a provided Character
fill_horizontal_line_with_size(...)Fills a horizontal line on the surface with a specific length. The line will be filled from left to right with a provided Character
fill_vertical_line_with_size(...)Fills a vertical line on the surface with a specific length. The line will be filled from top to bottom with a provided Character
fill_line(...)Fills a line between two points with a given Character (Bresenham, including diagonals)

Example:

#![allow(unused)]
fn main() {
use appcui::graphics::{Surface, CharAttribute, Color, Character};

let mut surface = Surface::new(100, 50);
let c = Character::new('=', Color::White, Color::Black, CharFlags::None);
surface.fill_horizontal_line(10, 10, 20, c);
}