You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/blog/posts/release1.0.0.md
+30-24Lines changed: 30 additions & 24 deletions
Original file line number
Diff line number
Diff line change
@@ -3,7 +3,7 @@ draft: true
3
3
date: 2024-12-05
4
4
categories:
5
5
- Release
6
-
title: "Three algorithms for a high performance terminal apps"
6
+
title: "Three algorithms for high performance terminal apps"
7
7
authors:
8
8
- willmcgugan
9
9
---
@@ -22,8 +22,7 @@ Often frustrating but never boring, the challenges arise because the terminal "s
22
22
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.
23
23
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.
24
24
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.
27
26
28
27
If you haven't followed along with Textual's development, here is a demo of what it can do.
29
28
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*.
46
45
The job of the compositor is to combine widgets in to a single view.
47
46
48
47
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.
50
49
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:
@@ -68,13 +67,15 @@ Anything you print in Rich, first generates a list of [Segments](https://github.
68
67
These Segments are only converted into text with [ansi escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code) at the very last moment.
69
68
70
69
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,.
73
73
74
74
!!! tip "Switch the Primitive"
75
75
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.
77
77
I call this "switching the primitive".
78
+
In Rich this was switching from thinking in characters to thinking in segments.
78
79
79
80
### Thinking in Segments
80
81
@@ -101,14 +102,15 @@ It would appear something like the following:
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.
105
106
106
107
We need a few more steps to combine these lines in to a single line.
107
108
108
109
109
110
### Step 1. Finding the cuts.
110
111
111
112
First thing the compositor does is to find every offset where a list of segments begins or ends.
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.
149
150
150
151
### What I omitted
151
152
152
153
There is more going on than this explanation may suggest.
153
154
Widgets may contain other widgets which are clipped to their *parent's* boundaries, and widgets that contain other widgets may also scroll — the compositor must take all of this in to account.
154
155
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.
157
162
158
163
The compositor does all of this fast enough to enable smooth scrolling, even with a metric tonne of widgets on screen.
159
164
@@ -174,7 +179,7 @@ Not all of which widgets may be visible in the final view (if they are within a
174
179
If you can think of a game that can be played in 2 characters — let me know!
175
180
176
181
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.
178
183
179
184
180
185
### The problem
@@ -185,8 +190,8 @@ Consider the following arrangement of widgets:
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.
190
195
191
196
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.
192
197
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
223
228
### Search the grid
224
229
225
230
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— 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— which may be the entire screen, or a smaller scrollable container.
227
232
228
233
In the following illustration we have scrolled the screen up[^3] a little so that Widget 3 is at the top of the screen:
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)`.
235
241
Once we have that information, we can then then look up those coordinates in the spatial map data structure, which would retrieve 4 lists:
236
242
237
243
```python
@@ -250,7 +256,7 @@ Combining those together and de-duplicating we get:
250
256
```
251
257
252
258
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.
254
260
If we need to know precisely which widgets are visible we can check their regions individually.
255
261
256
262
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