Text
A Text is a leaf element that draws text.
Layout sizes it by measuring its content, and wraps it only when its style sets a wrap mode:
see Text in layout for how a Text takes space,
and Text wrapping for the wrap modes.
Styling Parts of the Text
A Text's content is a string, drawn in the Text's text_style,
or a tuple of Chunks, each drawn in its own style merged over the Text's text_style,
for text whose style changes partway through, as a <span> inside a <p> is.
Whatever a chunk's style leaves unset, such as the foreground of the plain chunks below, comes from the Text.
A newline in a chunk starts a new line, as it does in a string,
and a wrap mode wraps the chunks as one run of text, so a word split across two chunks wraps as one word.
@component
def chunks() -> Div:
return Div(
style=col,
children=(
Text(content="A plain string, in the Text's text_style", style=text_color("slate", 300)),
Text(
style=text_color("slate", 300),
content=(
Chunk(content="error", style=CellStyle(foreground=red_500, bold=True)),
Chunk(content=": "),
Chunk(content="config.toml", style=CellStyle(underline=True)),
Chunk(content=" not found"),
),
),
),
)
The chunks keep the Text's slate foreground except where they set their own:
error is red and bold, and config.toml is underlined in slate.
A chunk's style can be a CellStyle, as above, or a Style, of which it takes the text_style,
so the utilities that style a whole Text style a chunk the same way:
WARNING = text_bold | text_color("amber", 400)
ASIDE = text_italic | text_dim
@component
def root() -> Div:
return Div(
style=col | gap(1) | pad_x(1),
children=(
Text(
content=(
Chunk(content="warning: ", style=WARNING),
Chunk(content="3 tests were "),
Chunk(content="skipped", style=text_underline | text_color(rose_400)),
Chunk(content=" (run with -v to see why)", style=ASIDE),
),
style=text_wrap_pretty,
),
Text(
content=(
Chunk(content="This whole Text is bold and sky blue, "),
Chunk(content="except this", style=text_not_bold | text_color("slate", 400)),
Chunk(content="."),
),
style=text_bold | text_color("sky", 300) | text_wrap_pretty,
),
),
)
The second Text is bold, so the chunk that shouldn't be merges text_not_bold over it.
A chunk ignores the rest of a Style, such as padding or a border, as a browser ignores a <span>'s width,
so put those on the Text or a Div around it.
Chunk.style holds the CellStyle either way.
Syntax highlighting and Markdown
build their output from chunks too, and a chunk can be a hyperlink.
Wide Characters
A Text measures, wraps and paints its content by grapheme cluster,
the unit a reader sees as one character,
so an accented letter written with a combining mark
or an emoji joined from several (๐ฉโ๐ป) stays whole.
Counterweight measures each cluster with wcwidth's wcswidth.
Most clusters take one terminal cell.
CJK characters, most emoji and flags take two,
and zero-width characters (zero-width spaces and joiners, bidirectional marks) take none and aren't drawn.
No cluster takes more than two,
which matches terminals that draw a cluster as one unit, such as Ghostty, foot and Windows Terminal.
Terminals that add up the widths of a cluster's code points instead
draw some clusters wider, such as a joined emoji as its separate parts,
and text after one is drawn to the right of where layout put it.
A two-cell cluster is never split across lines or cut in half.
When one would cross the right edge of its Text,
it moves to the next line if the text wraps,
or overflows the box with the rest of its line if it doesn't.
Where a clip cuts across one, it is left out, with a space in its place.
WIDE = "ๆฅๆฌ่ชใฎๆ็ซ ๐ฉโ๐ป ๐ฏ๐ต"
def pane(label: str, text_style: Style, content_width: int) -> Div:
return Div(
style=col | width(content_width + 2),
children=(
Text(content=label),
Div(style=col | border, children=(Text(style=text_style, content=WIDE),)),
),
)
@component
def wide() -> Div:
return Div(
style=row | align_children_start | gap(2),
children=(
pane("wraps", text_wrap_stable, 10),
pane("clipped", overflow_hidden, 9),
pane("overflows", default, 9),
),
)
wraps clipped overflows
โโโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโ
โๆฅๆฌ่ชใฎ- โ โๆฅๆฌ่ชใฎ โ โๆฅๆฌ่ชใฎๆ็ซ ๐ฉโ๐ป ๐ฏ๐ต
โๆ็ซ ๐ฉโ๐ป ๐ฏ๐ตโ โโโโโโโโโโโ โโโโโโโโโโโ
โโโโโโโโโโโโ
- In
wraps, ten cells wide, the first word is too long for a line, so it's broken with a hyphen. That leaves nine cells for the word, andๆwould take the ninth and tenth, so it starts the next line and the first line ends a cell short. - In
clipped, nine cells wide,overflow_hiddencuts acrossๆ, which is left out, with a space in the ninth cell. - In
overflows, the unwrapped line runs past the pane's border.
To cut or pad text yourself, measure it as counterweight does,
with graphemes and grapheme_width
from counterweight.text, so your cuts land where layout's and paint's do.
Control Characters
Newlines (\n and \r\n) start a new line.
Every other control character, such as tab or escape,
is drawn as a symbol (โ, โ) rather than written to the terminal,
where it would run as a command instead of drawing anything.
@component
def control() -> Text:
return Text(content="name\tsize\nnotes.txt\t\x1b[31mred?\x1b[0m\r\nend\x07")
The tabs, the escape sequences that would have turned the text red, and the bell at the end
are each drawn as one symbol, while \n and \r\n break the lines.
visible gives the symbol a control character is drawn as.
To color text, use chunks instead of escape sequences.
To show a program's colored output, run it in a terminal emulator.
API
counterweight.elements.Text
dataclass
Text(
*,
content: str | tuple[Chunk, ...],
style: Style = _DEFAULT_STYLE,
on_input: InputHandler | None = None,
handle: ElementHandle | None = None,
)
A leaf element that shows text, sized by its text unless its style sizes it, and wrapped as its style says.
content
instance-attribute
The text, as a string drawn in the style's text_style, or as chunks that each style a run of it.
style
class-attribute
instance-attribute
style: Style = _DEFAULT_STYLE
How the element is laid out and drawn, and how its text is styled, justified and wrapped.
on_input
class-attribute
instance-attribute
on_input: InputHandler | None = None
The handler for the input events that reach this element, as their target or as they bubble up from a descendant: key presses and pastes while it is focused, and mouse events over the parts of it that are painted.
handle
class-attribute
instance-attribute
handle: ElementHandle | None = None
The element handle that names this element for controls such as ScrollTo,
and for use_scroll.
counterweight.elements.Chunk
dataclass
A run of a Text's content in one style, as a <span> is in HTML.
style can be a CellStyle, or a Style whose text_style the chunk takes,
so the utilities that style a Text's text, such as text_bold | text_color("sky", 300), style a chunk too.
A chunk ignores the rest of a Style, as a <span> ignores a width,
and Chunk.style is always the CellStyle.
counterweight.text.graphemes
text split into extended grapheme clusters, the units a reader sees as one character.
counterweight.text.grapheme_width
cached
How many terminal cells a grapheme cluster takes, as wcwidth measures it: 0 (zero-width spaces and joiners, bidirectional marks, a combining mark with nothing to combine with), 1, or 2 (CJK, most emoji, flags).
A line break takes none. Any other control character raises ValueError, since terminals don't draw it;
pass it through visible first.
counterweight.text.visible
cached
grapheme, or a symbol standing in for it if it's a control character other than a line break,
since writing a control character to the terminal runs it as a command rather than drawing it.
C0 controls (including tab) and delete become their Control Pictures (โ, โ, โก),
and C1 controls, which have none, become ๏ฟฝ.