0
# Content and Rendering
1
2
Rich content system for displaying formatted text, custom renderables for specialized visualizations, content processing utilities, and DOM manipulation functions for widget tree traversal.
3
4
## Capabilities
5
6
### Rich Content System
7
8
Textual's content system allows for rich text display with styling spans and formatting.
9
10
```python { .api }
11
class Content:
12
"""Rich text content with styling spans."""
13
14
def __init__(self, text: str = "", spans: list[Span] | None = None):
15
"""
16
Initialize content.
17
18
Parameters:
19
- text: Plain text content
20
- spans: List of styling spans
21
"""
22
23
def __str__(self) -> str:
24
"""Get plain text representation."""
25
26
def __len__(self) -> int:
27
"""Get content length."""
28
29
def copy(self) -> Content:
30
"""Create a copy of the content."""
31
32
def append(self, text: str, *, style: Style | None = None) -> None:
33
"""
34
Append text with optional styling.
35
36
Parameters:
37
- text: Text to append
38
- style: Rich Style object for formatting
39
"""
40
41
def append_text(self, text: str) -> None:
42
"""Append plain text without styling."""
43
44
def extend(self, content: Content) -> None:
45
"""
46
Extend with another Content object.
47
48
Parameters:
49
- content: Content to append
50
"""
51
52
def split(self, separator: str = "\n") -> list[Content]:
53
"""
54
Split content by separator.
55
56
Parameters:
57
- separator: String to split on
58
59
Returns:
60
List of Content objects
61
"""
62
63
def truncate(self, max_length: int, *, overflow: str = "ellipsis") -> Content:
64
"""
65
Truncate content to maximum length.
66
67
Parameters:
68
- max_length: Maximum character length
69
- overflow: How to handle overflow ("ellipsis", "ignore")
70
71
Returns:
72
Truncated content
73
"""
74
75
# Properties
76
text: str # Plain text without styling
77
spans: list[Span] # List of styling spans
78
cell_length: int # Length in terminal cells
79
80
class Span:
81
"""Text styling span."""
82
83
def __init__(self, start: int, end: int, style: Style):
84
"""
85
Initialize a span.
86
87
Parameters:
88
- start: Start character index
89
- end: End character index
90
- style: Rich Style object
91
"""
92
93
def __contains__(self, index: int) -> bool:
94
"""Check if index is within span."""
95
96
# Properties
97
start: int # Start character index
98
end: int # End character index
99
style: Style # Rich Style object
100
```
101
102
### Strip Rendering
103
104
Strip system for efficient line-by-line rendering in Textual.
105
106
```python { .api }
107
class Strip:
108
"""Horizontal line of styled text cells."""
109
110
def __init__(self, segments: list[Segment] | None = None):
111
"""
112
Initialize strip.
113
114
Parameters:
115
- segments: List of styled text segments
116
"""
117
118
@classmethod
119
def blank(cls, length: int, *, style: Style | None = None) -> Strip:
120
"""
121
Create blank strip.
122
123
Parameters:
124
- length: Strip length in characters
125
- style: Optional styling
126
127
Returns:
128
Blank Strip instance
129
"""
130
131
def __len__(self) -> int:
132
"""Get strip length."""
133
134
def __bool__(self) -> bool:
135
"""Check if strip has content."""
136
137
def crop(self, start: int, end: int | None = None) -> Strip:
138
"""
139
Crop strip to range.
140
141
Parameters:
142
- start: Start index
143
- end: End index (None for end of strip)
144
145
Returns:
146
Cropped strip
147
"""
148
149
def extend(self, strips: Iterable[Strip]) -> Strip:
150
"""
151
Extend with other strips.
152
153
Parameters:
154
- strips: Strips to append
155
156
Returns:
157
Extended strip
158
"""
159
160
def apply_style(self, style: Style) -> Strip:
161
"""
162
Apply style to entire strip.
163
164
Parameters:
165
- style: Style to apply
166
167
Returns:
168
Styled strip
169
"""
170
171
# Properties
172
text: str # Plain text content
173
cell_length: int # Length in terminal cells
174
175
class Segment:
176
"""Text segment with styling."""
177
178
def __init__(self, text: str, style: Style | None = None):
179
"""
180
Initialize segment.
181
182
Parameters:
183
- text: Text content
184
- style: Optional Rich Style
185
"""
186
187
# Properties
188
text: str # Text content
189
style: Style | None # Styling information
190
```
191
192
### Custom Renderables
193
194
Specialized rendering components for charts, progress bars, and other visualizations.
195
196
```python { .api }
197
class Bar:
198
"""Progress bar renderable."""
199
200
def __init__(
201
self,
202
size: Size,
203
*,
204
highlight_range: tuple[float, float] | None = None,
205
foreground_style: Style | None = None,
206
background_style: Style | None = None,
207
complete_style: Style | None = None,
208
):
209
"""
210
Initialize bar renderable.
211
212
Parameters:
213
- size: Bar dimensions
214
- highlight_range: Range to highlight (0.0-1.0)
215
- foreground_style: Foreground styling
216
- background_style: Background styling
217
- complete_style: Completed portion styling
218
"""
219
220
def __rich_console__(self, console, options) -> RenderResult:
221
"""Render the bar for Rich console."""
222
223
class Digits:
224
"""Digital display renderable for large numbers."""
225
226
def __init__(
227
self,
228
text: str,
229
*,
230
style: Style | None = None
231
):
232
"""
233
Initialize digits display.
234
235
Parameters:
236
- text: Text/numbers to display
237
- style: Display styling
238
"""
239
240
def __rich_console__(self, console, options) -> RenderResult:
241
"""Render digits for Rich console."""
242
243
class Sparkline:
244
"""Sparkline chart renderable."""
245
246
def __init__(
247
self,
248
data: Sequence[float],
249
*,
250
width: int | None = None,
251
min_color: Color | None = None,
252
max_color: Color | None = None,
253
summary_color: Color | None = None,
254
):
255
"""
256
Initialize sparkline.
257
258
Parameters:
259
- data: Numeric data points
260
- width: Chart width (None for auto)
261
- min_color: Color for minimum values
262
- max_color: Color for maximum values
263
- summary_color: Color for summary statistics
264
"""
265
266
def __rich_console__(self, console, options) -> RenderResult:
267
"""Render sparkline for Rich console."""
268
269
class Gradient:
270
"""Color gradient renderable."""
271
272
def __init__(
273
self,
274
size: Size,
275
stops: Sequence[tuple[float, Color]],
276
direction: str = "horizontal"
277
):
278
"""
279
Initialize gradient.
280
281
Parameters:
282
- size: Gradient dimensions
283
- stops: Color stops as (position, color) tuples
284
- direction: "horizontal" or "vertical"
285
"""
286
287
def __rich_console__(self, console, options) -> RenderResult:
288
"""Render gradient for Rich console."""
289
290
class Blank:
291
"""Empty space renderable."""
292
293
def __init__(self, width: int, height: int, *, style: Style | None = None):
294
"""
295
Initialize blank space.
296
297
Parameters:
298
- width: Width in characters
299
- height: Height in lines
300
- style: Optional background styling
301
"""
302
303
def __rich_console__(self, console, options) -> RenderResult:
304
"""Render blank space for Rich console."""
305
```
306
307
### DOM Tree Traversal
308
309
Utilities for walking and querying the widget DOM tree.
310
311
```python { .api }
312
def walk_children(
313
node: DOMNode,
314
*,
315
reverse: bool = False,
316
with_root: bool = True
317
) -> Iterator[DOMNode]:
318
"""
319
Walk immediate children of a DOM node.
320
321
Parameters:
322
- node: Starting DOM node
323
- reverse: Walk in reverse order
324
- with_root: Include the root node
325
326
Yields:
327
Child DOM nodes
328
"""
329
330
def walk_depth_first(
331
root: DOMNode,
332
*,
333
reverse: bool = False,
334
with_root: bool = True
335
) -> Iterator[DOMNode]:
336
"""
337
Walk DOM tree depth-first.
338
339
Parameters:
340
- root: Root node to start from
341
- reverse: Traverse in reverse order
342
- with_root: Include the root node
343
344
Yields:
345
DOM nodes in depth-first order
346
"""
347
348
def walk_breadth_first(
349
root: DOMNode,
350
*,
351
reverse: bool = False,
352
with_root: bool = True
353
) -> Iterator[DOMNode]:
354
"""
355
Walk DOM tree breadth-first.
356
357
Parameters:
358
- root: Root node to start from
359
- reverse: Traverse in reverse order
360
- with_root: Include the root node
361
362
Yields:
363
DOM nodes in breadth-first order
364
"""
365
366
class DOMNode:
367
"""Base DOM node with CSS query support."""
368
369
def query(self, selector: str) -> DOMQuery[DOMNode]:
370
"""
371
Query descendant nodes with CSS selector.
372
373
Parameters:
374
- selector: CSS selector string
375
376
Returns:
377
Query result set
378
"""
379
380
def query_one(
381
self,
382
selector: str,
383
expected_type: type[ExpectedType] = DOMNode
384
) -> ExpectedType:
385
"""
386
Query for single descendant node.
387
388
Parameters:
389
- selector: CSS selector string
390
- expected_type: Expected node type
391
392
Returns:
393
Single matching node
394
395
Raises:
396
NoMatches: If no nodes match
397
WrongType: If node is wrong type
398
TooManyMatches: If multiple nodes match
399
"""
400
401
def remove_class(self, *class_names: str) -> None:
402
"""
403
Remove CSS classes.
404
405
Parameters:
406
- *class_names: Class names to remove
407
"""
408
409
def add_class(self, *class_names: str) -> None:
410
"""
411
Add CSS classes.
412
413
Parameters:
414
- *class_names: Class names to add
415
"""
416
417
def toggle_class(self, *class_names: str) -> None:
418
"""
419
Toggle CSS classes.
420
421
Parameters:
422
- *class_names: Class names to toggle
423
"""
424
425
def has_class(self, class_name: str) -> bool:
426
"""
427
Check if node has CSS class.
428
429
Parameters:
430
- class_name: Class name to check
431
432
Returns:
433
True if class is present
434
"""
435
436
# Properties
437
id: str | None # Unique identifier
438
classes: set[str] # CSS classes
439
parent: DOMNode | None # Parent node
440
children: list[DOMNode] # Child nodes
441
ancestors: list[DOMNode] # All ancestor nodes
442
ancestors_with_self: list[DOMNode] # Ancestors including self
443
444
class DOMQuery:
445
"""Result set from DOM queries."""
446
447
def __init__(self, nodes: Iterable[DOMNode]):
448
"""
449
Initialize query result.
450
451
Parameters:
452
- nodes: Matching DOM nodes
453
"""
454
455
def __len__(self) -> int:
456
"""Get number of matching nodes."""
457
458
def __iter__(self) -> Iterator[DOMNode]:
459
"""Iterate over matching nodes."""
460
461
def __bool__(self) -> bool:
462
"""Check if query has results."""
463
464
def first(self, expected_type: type[ExpectedType] = DOMNode) -> ExpectedType:
465
"""
466
Get first matching node.
467
468
Parameters:
469
- expected_type: Expected node type
470
471
Returns:
472
First matching node
473
"""
474
475
def last(self, expected_type: type[ExpectedType] = DOMNode) -> ExpectedType:
476
"""
477
Get last matching node.
478
479
Parameters:
480
- expected_type: Expected node type
481
482
Returns:
483
Last matching node
484
"""
485
486
def remove(self) -> None:
487
"""Remove all matching nodes from DOM."""
488
489
def add_class(self, *class_names: str) -> None:
490
"""Add CSS classes to all matching nodes."""
491
492
def remove_class(self, *class_names: str) -> None:
493
"""Remove CSS classes from all matching nodes."""
494
495
def set_styles(self, **styles) -> None:
496
"""Set styles on all matching nodes."""
497
498
class NoMatches(Exception):
499
"""Raised when DOM query finds no matches."""
500
501
class WrongType(Exception):
502
"""Raised when DOM node is wrong type."""
503
504
class TooManyMatches(Exception):
505
"""Raised when DOM query finds too many matches."""
506
```
507
508
### Content Processing Utilities
509
510
```python { .api }
511
def strip_links(content: Content) -> Content:
512
"""
513
Remove all links from content.
514
515
Parameters:
516
- content: Content to process
517
518
Returns:
519
Content with links removed
520
"""
521
522
def highlight_words(
523
content: Content,
524
words: Iterable[str],
525
*,
526
style: Style,
527
case_sensitive: bool = False
528
) -> Content:
529
"""
530
Highlight specific words in content.
531
532
Parameters:
533
- content: Content to process
534
- words: Words to highlight
535
- style: Highlight style
536
- case_sensitive: Whether matching is case sensitive
537
538
Returns:
539
Content with highlighted words
540
"""
541
542
def truncate_middle(
543
text: str,
544
length: int,
545
*,
546
ellipsis: str = "…"
547
) -> str:
548
"""
549
Truncate text in the middle.
550
551
Parameters:
552
- text: Text to truncate
553
- length: Target length
554
- ellipsis: Ellipsis character
555
556
Returns:
557
Truncated text
558
"""
559
```
560
561
## Usage Examples
562
563
### Rich Content Creation
564
565
```python
566
from textual.app import App
567
from textual.widgets import Static
568
from textual.content import Content, Span
569
from rich.style import Style
570
571
class ContentApp(App):
572
def compose(self):
573
# Create rich content with spans
574
content = Content("Hello, bold world!")
575
content.spans.append(
576
Span(7, 11, Style(bold=True, color="red"))
577
)
578
579
yield Static(content, id="rich-text")
580
581
# Alternative: build content incrementally
582
content2 = Content()
583
content2.append("Normal text ")
584
content2.append("highlighted", style=Style(bgcolor="yellow"))
585
content2.append(" and back to normal")
586
587
yield Static(content2, id="incremental")
588
589
def on_mount(self):
590
# Manipulate content after creation
591
static = self.query_one("#rich-text", Static)
592
current_content = static.renderable
593
594
# Truncate if too long
595
if len(current_content.text) > 20:
596
truncated = current_content.truncate(20)
597
static.update(truncated)
598
```
599
600
### Custom Renderables
601
602
```python
603
from textual.app import App
604
from textual.widgets import Static
605
from textual.renderables import Sparkline, Digits, Bar
606
from textual.geometry import Size
607
608
class RenderableApp(App):
609
def compose(self):
610
# Sparkline chart
611
data = [1, 3, 2, 5, 4, 6, 3, 2, 4, 1]
612
sparkline = Sparkline(data, width=20)
613
yield Static(sparkline, id="chart")
614
615
# Digital display
616
digits = Digits("12:34")
617
yield Static(digits, id="clock")
618
619
# Progress bar
620
bar = Bar(Size(30, 1), highlight_range=(0.0, 0.7))
621
yield Static(bar, id="progress")
622
623
def update_displays(self):
624
"""Update the displays with new data."""
625
import time
626
627
# Update clock
628
current_time = time.strftime("%H:%M")
629
digits = Digits(current_time)
630
self.query_one("#clock", Static).update(digits)
631
632
# Update progress
633
import random
634
progress = random.random()
635
bar = Bar(Size(30, 1), highlight_range=(0.0, progress))
636
self.query_one("#progress", Static).update(bar)
637
```
638
639
### DOM Tree Traversal
640
641
```python
642
from textual.app import App
643
from textual.containers import Container, Horizontal
644
from textual.widgets import Button, Static
645
from textual.walk import walk_depth_first, walk_breadth_first
646
647
class TraversalApp(App):
648
def compose(self):
649
with Container(id="main"):
650
yield Static("Header", id="header")
651
with Horizontal(id="buttons"):
652
yield Button("Button 1", id="btn1")
653
yield Button("Button 2", id="btn2")
654
yield Static("Footer", id="footer")
655
656
def on_mount(self):
657
# Walk all widgets depth-first
658
main_container = self.query_one("#main")
659
660
self.log("Depth-first traversal:")
661
for widget in walk_depth_first(main_container):
662
self.log(f" {widget.__class__.__name__}: {widget.id}")
663
664
self.log("Breadth-first traversal:")
665
for widget in walk_breadth_first(main_container):
666
self.log(f" {widget.__class__.__name__}: {widget.id}")
667
668
def on_button_pressed(self, event: Button.Pressed):
669
# Find all buttons in the app
670
all_buttons = self.query("Button")
671
self.log(f"Found {len(all_buttons)} buttons")
672
673
# Find parent container of clicked button
674
parent = event.button.parent
675
while parent and not parent.id == "main":
676
parent = parent.parent
677
678
if parent:
679
self.log(f"Button is in container: {parent.id}")
680
```
681
682
### CSS Queries and DOM Manipulation
683
684
```python
685
from textual.app import App
686
from textual.widgets import Button, Static
687
from textual.containers import Container
688
689
class QueryApp(App):
690
def compose(self):
691
with Container():
692
yield Static("Status: Ready", classes="status info")
693
yield Button("Success", classes="action success")
694
yield Button("Warning", classes="action warning")
695
yield Button("Error", classes="action error")
696
697
def on_button_pressed(self, event: Button.Pressed):
698
# Query by class
699
status = self.query_one(".status", Static)
700
701
# Update based on button type
702
if event.button.has_class("success"):
703
status.update("Status: Success!")
704
status.remove_class("info", "warning", "error")
705
status.add_class("success")
706
707
elif event.button.has_class("warning"):
708
status.update("Status: Warning!")
709
status.remove_class("info", "success", "error")
710
status.add_class("warning")
711
712
elif event.button.has_class("error"):
713
status.update("Status: Error!")
714
status.remove_class("info", "success", "warning")
715
status.add_class("error")
716
717
# Style all action buttons
718
action_buttons = self.query(".action")
719
action_buttons.set_styles(opacity=0.7)
720
721
# Highlight clicked button
722
event.button.styles.opacity = 1.0
723
```
724
725
### Strip-based Custom Widget
726
727
```python
728
from textual.widget import Widget
729
from textual.strip import Strip
730
from textual.geometry import Size
731
from rich.segment import Segment
732
from rich.style import Style
733
734
class ProgressWidget(Widget):
735
"""Custom widget using Strip rendering."""
736
737
def __init__(self, progress: float = 0.0):
738
super().__init__()
739
self.progress = max(0.0, min(1.0, progress))
740
741
def render_line(self, y: int) -> Strip:
742
"""Render a single line using Strip."""
743
if y != 0: # Only render on first line
744
return Strip.blank(self.size.width)
745
746
# Calculate progress bar dimensions
747
width = self.size.width
748
filled_width = int(width * self.progress)
749
750
# Create segments for filled and empty portions
751
segments = []
752
753
if filled_width > 0:
754
segments.append(
755
Segment("█" * filled_width, Style(color="green"))
756
)
757
758
if filled_width < width:
759
segments.append(
760
Segment("░" * (width - filled_width), Style(color="gray"))
761
)
762
763
return Strip(segments)
764
765
def get_content_width(self, container: Size, viewport: Size) -> tuple[int, int]:
766
"""Get content width."""
767
return (20, 20) # Fixed width
768
769
def get_content_height(self, container: Size, viewport: Size, width: int) -> tuple[int, int]:
770
"""Get content height."""
771
return (1, 1) # Single line
772
```