Skip to content

Commit db28df7

Browse files
committed
words
1 parent cdffb6c commit db28df7

File tree

1 file changed

+30
-24
lines changed

1 file changed

+30
-24
lines changed

docs/blog/posts/release1.0.0.md

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ draft: true
33
date: 2024-12-05
44
categories:
55
- Release
6-
title: "Three algorithms for a high performance terminal apps"
6+
title: "Three algorithms for high performance terminal apps"
77
authors:
88
- willmcgugan
99
---
@@ -22,8 +22,7 @@ Often frustrating but never boring, the challenges arise because the terminal "s
2222
The building blocks are there: after some effort you can move the cursor, write colored text, read keys and mouse movements, but that's about it.
2323
Everything else we had to build from scratch, from the most basic [button](https://textual.textualize.io/widget_gallery/#button) to a syntax highlighted [TextArea](https://textual.textualize.io/widget_gallery/#textarea), and everything along the way.
2424

25-
I wanted to write-up some of the more interesting solutions we came up with for a while.
26-
The 1.0 milestone we just passed makes this the perfect time.
25+
I wanted to write-up some of the more interesting solutions we came up with, and the 1.0 milestone we just passed makes this a perfect time.
2726

2827
If you haven't followed along with Textual's development, here is a demo of what it can do.
2928
This is a Textual app, running remotely, served by your browser:
@@ -46,9 +45,9 @@ The first component of Textual I want to cover is the *compositor*.
4645
The job of the compositor is to combine widgets in to a single view.
4746

4847
We do this because the terminal itself has no notion of overlapping windows in the way a desktop does.
49-
If an app wants to display overlapping components it must combine them into a single update.
48+
If an app wants to display overlapping components we must first combine (or compose) them into a single update.
5049

51-
Here's a video I generated over a year ago, demonstrating the output of the compositor:
50+
Here's a video I generated over a year ago, demonstrating the compositor:
5251

5352
<div class="video-wrapper">
5453
<iframe width="100%" height="auto" src="https://www.youtube.com/embed/T8PZjUVVb50" title="" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
@@ -68,13 +67,15 @@ Anything you print in Rich, first generates a list of [Segments](https://github.
6867
These Segments are only converted into text with [ansi escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code) at the very last moment.
6968

7069

71-
Textual works with same Segment object.
72-
Widgets all produce a list of segments, which is further processed by the compositor.
70+
Textual builds its output from the same Segment object.
71+
The compositor takes lists of segments generated by widgets and further processes them, by dividing and combining, to produce the final output.
72+
In fact almost everything Textual does involves processing these segments in one way or another,.
7373

7474
!!! tip "Switch the Primitive"
7575

76-
If a problem is intractable, it can often be simplified by changing what you consider to be the fundamental atomic data and operations you are working with.
76+
If a problem is intractable, it can often be simplified by changing what you consider to be the atomic data and operations you are working with.
7777
I call this "switching the primitive".
78+
In Rich this was switching from thinking in characters to thinking in segments.
7879

7980
### Thinking in Segments
8081

@@ -101,14 +102,15 @@ It would appear something like the following:
101102
--8<-- "docs/blog/images/compositor/cuts0.excalidraw.svg"
102103
</div>
103104

104-
We can't yet display the output as it would require writing each "layer" independantly, potentially making the terminal flicker, and certainly writing more data than neccesary.
105+
We can't yet display the output as it would require writing each "layer" independently, potentially making the terminal flicker, and certainly writing more data than necessary.
105106

106107
We need a few more steps to combine these lines in to a single line.
107108

108109

109110
### Step 1. Finding the cuts.
110111

111112
First thing the compositor does is to find every offset where a list of segments begins or ends.
113+
We call these "cuts".
112114

113115
<div class="excalidraw">
114116
--8<-- "docs/blog/images/compositor/cuts1.excalidraw.svg"
@@ -117,15 +119,14 @@ First thing the compositor does is to find every offset where a list of segments
117119
### Step 2. Applying the cuts.
118120

119121
The next step is to divide every list of segments at the cut offsets.
120-
This will produce smaller lists of segments, which in the compositor code we refer to as *chops*.
122+
This will produce smaller lists of segments, which we refer to as *chops*.
121123

122124
<div class="excalidraw">
123125
--8<-- "docs/blog/images/compositor/cuts2.excalidraw.svg"
124126
</div>
125127

126-
These chops have the property that nothing overlaps.
127-
If there is a chop below any give chop, then both will have the same size.
128-
This property makes the next step possible.
128+
After this step we have lists of chops where each chop is of the same size, and therefore nothing overlaps.
129+
It's the non-overlapping property that makes the next step possible.
129130

130131
### Step 3. Discard chops.
131132

@@ -138,22 +139,26 @@ Anything not at the top will be occluded and can be thrown away.
138139

139140
### Step 4. Combine.
140141

141-
Now all that's left is to combine the top-most chops in to a single list of Segments. It is this list of segments that becomes a line in the terminal.
142+
Now all that's left is to combine the top-most chops in to a single list of Segments.
143+
It is this list of segments that becomes a line in the terminal.
142144

143145
<div class="excalidraw">
144146
--8<-- "docs/blog/images/compositor/cuts4.excalidraw.svg"
145147
</div>
146148

147-
this is done for ever line in the output.
148-
At the end of it we have a list of segments for each line, ready to be converted in to escape sequences and written to the terminal.
149+
As this is the final step in the process, these lines of segments will ultimately be converted to text plus escape sequences and written to the output.
149150

150151
### What I omitted
151152

152153
There is more going on than this explanation may suggest.
153154
Widgets may contain other widgets which are clipped to their *parent's* boundaries, and widgets that contain other widgets may also scroll &mdash; the compositor must take all of this in to account.
154155

155-
Additionally, the compositor can do partial updates.
156-
In other words, if you click a button and it changes color the compositor can update just that button.
156+
!!! info "It's widgets all the way down"
157+
158+
Not to mention there can be multiple "screens" of widgets stacked on top of each other, with a modal fade effect applied to lower screens.
159+
160+
The compositor can do partial updates.
161+
In other words, if you click a button and it changes color, the compositor can update just the region occupied by the button.
157162

158163
The compositor does all of this fast enough to enable smooth scrolling, even with a metric tonne of widgets on screen.
159164

@@ -174,7 +179,7 @@ Not all of which widgets may be visible in the final view (if they are within a
174179
If you can think of a game that can be played in 2 characters &mdash; let me know!
175180

176181
The *spatial map*[^1] is a data structure used by the compositor to very quickly discard widgets that are not visible within a given region.
177-
The algorithm is uses may be familiar if you have done any classic game-dev.
182+
The algorithm it uses may be familiar if you have done any classic game-dev.
178183

179184

180185
### The problem
@@ -185,8 +190,8 @@ Consider the following arrangement of widgets:
185190
--8<-- "docs/blog/images/compositor/spatial-map.excalidraw.svg"
186191
</div>
187192

188-
Here we have 8 widgets, where only around 4 will be visible at any given time depending on the position of the scrollbar.
189-
We want to avoid doing any work on widgets which will not be seen in the next frame.
193+
Here we have 8 widgets, where only around 4 will be visible at any given time, depending on the position of the scrollbar.
194+
We want to avoid doing work on widgets which will not be seen in the next frame.
190195

191196
A naive solution to this would be to check each widget's [Region][textual.geometry.Region] to see if it overlaps with the visible area.
192197
This is a perfectly reasonable solution, but it won't scale well.
@@ -223,15 +228,16 @@ If the widgets don't change their position or size such as when user is *scrolli
223228
### Search the grid
224229

225230
The speedups from the spatial map come when we want to know which widgets are visible.
226-
To do that, we first create a region that covers the area that is scrolling &mdash; which may be the entire screen, or a smaller scrollable container.
231+
To do that, we first create a region that covers the area we want to consider &mdash; which may be the entire screen, or a smaller scrollable container.
227232

228233
In the following illustration we have scrolled the screen up[^3] a little so that Widget 3 is at the top of the screen:
229234

230235
<div class="excalidraw">
231236
--8<-- "docs/blog/images/compositor/spatial-map-view1.excalidraw.svg"
232237
</div>
233238

234-
We then determine which grid tiles are covered by the viewable area, which would be `(0,0)`, `(1,0)`, `(0,1)`, and `(1,1)`.
239+
We then determine which grid tiles overlap by the viewable area.
240+
In the above examples that would be the tiles with coordinates `(0,0)`, `(1,0)`, `(0,1)`, and `(1,1)`.
235241
Once we have that information, we can then then look up those coordinates in the spatial map data structure, which would retrieve 4 lists:
236242

237243
```python
@@ -250,7 +256,7 @@ Combining those together and de-duplicating we get:
250256
```
251257

252258
These widgets are either within the viewable area, or close by.
253-
The widgets not included in the list (`widget7`, `widget8`) we can confidently conclude that they are not visible.
259+
We can confidently conclude that the widgets *not* ion that list are hidden from view.
254260
If we need to know precisely which widgets are visible we can check their regions individually.
255261

256262
The useful property of this algorithm is that as the number of widgets increases, the time it takes to figure out which are visible stays relatively constant. Scrolling a view of 8 widgets, takes much the same time as a view of 1000 widgets or more.

0 commit comments

Comments
 (0)