words and images
21
docs/blog/images/compositor/cuts0.excalidraw.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 623.0110473632812 172.49923719399783" width="623.0110473632812" height="172.49923719399783">
|
||||
<!-- svg-source:excalidraw -->
|
||||
<!-- payload-type:application/vnd.excalidraw+json --><!-- payload-version:2 --><!-- payload-start -->eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nM2U22rbQFx1MDAxMIbv/Vx1MDAxNEa9bZQ9XHUwMDFmXHUwMDAypZA0hLTBXHI4pTilXHUwMDE0WVpZQrKkSms7aci7d6Q4XsWtQ1posS5cdTAwMTZ2Znfmn9lPczdcdTAwMThcdTAwMGU9e1tcdTAwMTnvaOiZmzDI06hcdTAwMGVW3uvWvjR1k5ZcdTAwMDW4SLdvykVcdTAwMWR2J1x1MDAxM2ur5ujwcFx1MDAxZdSZsVVcdTAwMWWExl+mzVwiyFx1MDAxYruI0tJcdTAwMGbL+WFqzbx5266jYG7eVOU8srXvklx1MDAxY5gotWX9kMvkZm5cbttA9C+wXHUwMDFmXHUwMDBl77q1p642oVxyilluulx1MDAwYp3LXHTUets4KotOq4KPY6Tp5kDavINs1kTgjUGxcZ7W5H2Y1MnFdzxNwtPl9XjUyNE0XHUwMDFiuaRxmudje5t3oppcdTAwMTJqcb7G1mVmPqeRTVx1MDAxZbvWs++6VZeLWVKYpi1cdTAwMWVvrGVcdTAwMTWEqb1tbVxibaxcdTAwMGZcdTAwMWQ4XHUwMDFhOstccuxcdTAwMGVcdTAwMDT1OeKcUKGwYEhcdTAwMTC+8bdcdTAwMTEolz5WmnGmpaBcdTAwMTRJviXtpMzhJUDaK6wlXHUwMDBliVx1MDAxMzdccsJsXHUwMDA2XG6LyJ1cdHik4tidWa1cdTAwMGJcdTAwMTaI+lxiY8QkXHUwMDE1lCjsXHUwMDFhkJh0lti2J8pXjDGCqVx1MDAxMlx1MDAxMqu+XHUwMDEw072IpFxuMyWE3Dja7NV51KHx1T1DXHJQnbc3ikWe93tZROtePlwi5CCia8u9K689f9qDz2VYVFHwQFx0lpQyLFxiVko6jvK0yLbT52WYObBcdTAwMDa9XFx/xjMnZFx1MDAxN9BcdTAwMWEhXHQ4I/xioC+zM/w+mVxcXHUwMDEwXHUwMDFkf7sqP02v8/LjeN+BJj5RQkO/XHUwMDExXHUwMDEzfUza+0Qz4ExcdTAwMTNcIjXhwPRumkmsXHJjz9M8JTGZTn+lmXLkKy4lZtBvIVXvn3I4XHUwMDBin1x0LFx1MDAxMFxmmVx1MDAwZWe2jTOmXHUwMDE4PEyjveSZw9/2X3iGXHUwMDE3l7uAplx1MDAxMlx0xEHNy4E+WY1XJrnKJseJvsTV5KxcdTAwMTlcdTAwMWTsN9BcdTAwMThrXHUwMDFmXHUwMDAxXGJcbmhVhCqknlx1MDAxMk1hgFMmqMZCI457429cdTAwMWJpgyhA9TzScVx1MDAxY+pQ/yOkXHUwMDA1jG8msaD7SDTBfzGhYe2CekFVjS2E3EiD0tJonP4wT8J4y9Ssjn/b9/bzXHUwMDA2a/0tiaar835w/1x1MDAxM/xcdTAwMThbnyJ9<!-- payload-end -->
|
||||
<defs>
|
||||
<style class="style-fonts">
|
||||
@font-face {
|
||||
font-family: "Virgil";
|
||||
src: url("https://file%2B.vscode-resource.vscode-cdn.net/Users/willmcgugan/.vscode/extensions/pomdtr.excalidraw-editor-3.7.4/public//dist/excalidraw-assets/Virgil.woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Cascadia";
|
||||
src: url("https://file%2B.vscode-resource.vscode-cdn.net/Users/willmcgugan/.vscode/extensions/pomdtr.excalidraw-editor-3.7.4/public//dist/excalidraw-assets/Cascadia.woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Assistant";
|
||||
src: url("https://file%2B.vscode-resource.vscode-cdn.net/Users/willmcgugan/.vscode/extensions/pomdtr.excalidraw-editor-3.7.4/public//dist/excalidraw-assets/Assistant-Regular.woff2");
|
||||
}
|
||||
</style>
|
||||
|
||||
</defs>
|
||||
<rect x="0" y="0" width="623.0110473632812" height="172.49923719399783" fill="#ffffff"></rect><g stroke-linecap="round" transform="translate(10 133.65481580727908) rotate(0 301.5055236816406 14.422210693359375)"><path d="M7.21 0 C153.8 1.19, 300.24 1.5, 595.8 0 C599.7 3.6, 604.82 -0.83, 603.01 7.21 C603.55 10.8, 600.38 13.49, 603.01 21.63 C601.83 25.14, 598.51 26.61, 595.8 28.84 C462.34 25.32, 327.98 25.56, 7.21 28.84 C3.48 26.79, -2.06 23.32, 0 21.63 C-1.23 16.03, 1.94 10.15, 0 7.21 C1.99 2.32, 4.84 1.65, 7.21 0" stroke="none" stroke-width="0" fill="#a5d8ff"></path><path d="M7.21 0 C131.49 1.68, 256.9 1.46, 595.8 0 M7.21 0 C187.79 2.19, 367.96 2.56, 595.8 0 M595.8 0 C598.99 0.97, 604.55 2.5, 603.01 7.21 M595.8 0 C598.7 -1.96, 603.13 1.07, 603.01 7.21 M603.01 7.21 C603.95 12.35, 601.66 13.96, 603.01 21.63 M603.01 7.21 C603.48 12.17, 603.53 18.82, 603.01 21.63 M603.01 21.63 C604.26 28.11, 600.97 29.34, 595.8 28.84 M603.01 21.63 C602.96 28.33, 599.84 29.21, 595.8 28.84 M595.8 28.84 C458.45 28.69, 319.07 28.35, 7.21 28.84 M595.8 28.84 C447.81 26.6, 300.16 27.23, 7.21 28.84 M7.21 28.84 C3.28 29.1, -0.43 27.58, 0 21.63 M7.21 28.84 C2.01 29.02, 1.9 24.45, 0 21.63 M0 21.63 C1.1 16, 0.95 10.94, 0 7.21 M0 21.63 C0.85 16.58, 0.69 12.1, 0 7.21 M0 7.21 C-0.1 3.34, 2.5 -1.83, 7.21 0 M0 7.21 C-0.31 2.23, 2.09 0.6, 7.21 0" stroke="#1971c2" stroke-width="2" fill="none"></path></g><g stroke-linecap="round" transform="translate(11.218536376953125 70.48458875649783) rotate(0 175.42885735483912 13.230804443359375)"><path d="M6.62 0 C91.52 1.52, 175.9 4.86, 344.24 0 C350.46 -3.23, 349.59 0.49, 350.86 6.62 C348.62 10.65, 350.14 16.33, 350.86 19.85 C348.76 22.02, 346.06 25.93, 344.24 26.46 C264.64 22.58, 183.37 22.39, 6.62 26.46 C0.15 23.34, 1.07 22.34, 0 19.85 C1.78 14.81, 1.44 13.33, 0 6.62 C2.43 3.85, 3.06 0.6, 6.62 0" stroke="none" stroke-width="0" fill="#b2f2bb"></path><path d="M6.62 0 C81.03 -0.09, 153.93 1.16, 344.24 0 M6.62 0 C76.19 0.52, 146.43 0.18, 344.24 0 M344.24 0 C350.19 0.09, 349.2 0.5, 350.86 6.62 M344.24 0 C348.77 -1.34, 350.19 1.73, 350.86 6.62 M350.86 6.62 C349.73 10.18, 352.02 15.66, 350.86 19.85 M350.86 6.62 C351.43 10.61, 351.36 14.04, 350.86 19.85 M350.86 19.85 C351.22 24.75, 348.6 28.1, 344.24 26.46 M350.86 19.85 C350.09 24.63, 347.15 26.23, 344.24 26.46 M344.24 26.46 C246.39 26.62, 150.1 26.7, 6.62 26.46 M344.24 26.46 C271.17 25.35, 197.87 24.87, 6.62 26.46 M6.62 26.46 C1.78 27.6, -0.34 24.41, 0 19.85 M6.62 26.46 C4.1 24.47, 0.57 24.22, 0 19.85 M0 19.85 C0.77 14.51, 0 10.39, 0 6.62 M0 19.85 C0.38 15.53, -0.16 12.72, 0 6.62 M0 6.62 C0.09 0.38, 1.94 -0.15, 6.62 0 M0 6.62 C-0.31 2.81, 1.03 -0.35, 6.62 0" stroke="#2f9e44" stroke-width="2" fill="none"></path></g><g stroke-linecap="round" transform="translate(192.5730086398787 10) rotate(0 175.42885735483912 13.230804443359375)"><path d="M6.62 0 C87.01 3.99, 169.82 -0.59, 344.24 0 C347.38 -1.72, 354.44 3.13, 350.86 6.62 C349.85 9.61, 349.24 12.84, 350.86 19.85 C348.27 23.73, 346.47 28.48, 344.24 26.46 C265.99 23.08, 184.92 22.36, 6.62 26.46 C3.27 24.55, -2.01 22.35, 0 19.85 C1.15 16.25, 1.44 13.86, 0 6.62 C0.86 2.81, 3.33 2.32, 6.62 0" stroke="none" stroke-width="0" fill="#ffc9c9"></path><path d="M6.62 0 C97.63 1.79, 187.23 1.42, 344.24 0 M6.62 0 C109.53 -1.9, 214.03 -2.2, 344.24 0 M344.24 0 C346.99 -1.7, 350.96 1.04, 350.86 6.62 M344.24 0 C347.99 -0.47, 351.86 3.76, 350.86 6.62 M350.86 6.62 C351.81 11.66, 350.2 17.68, 350.86 19.85 M350.86 6.62 C351.37 10.15, 351.07 12.74, 350.86 19.85 M350.86 19.85 C350.81 25.9, 347.99 26.78, 344.24 26.46 M350.86 19.85 C349.35 24.02, 348.34 28.42, 344.24 26.46 M344.24 26.46 C235.52 25.75, 126.99 25.63, 6.62 26.46 M344.24 26.46 C268.25 28.47, 191.53 28.76, 6.62 26.46 M6.62 26.46 C1.86 26.61, 1.65 22.53, 0 19.85 M6.62 26.46 C2.77 26.43, 0.26 26, 0 19.85 M0 19.85 C0.02 15.53, -0.34 14.98, 0 6.62 M0 19.85 C-0.13 17.3, -0.07 13.54, 0 6.62 M0 6.62 C-0.27 2.05, 1.94 0.53, 6.62 0 M0 6.62 C-1.17 1.85, 1.09 0.09, 6.62 0" stroke="#e03131" stroke-width="2" fill="none"></path></g></svg>
|
||||
|
After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 38 KiB |
21
docs/blog/images/compositor/cuts4.excalidraw.svg
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
21
docs/blog/images/compositor/widgets.excalidraw.svg
Normal file
|
After Width: | Height: | Size: 16 KiB |
@@ -12,6 +12,116 @@ Version 1.0 announcements often come with a happy hour and merch.
|
||||
Well we haven't got any merch.
|
||||
|
||||
|
||||
## The Compositor
|
||||
|
||||
|
||||
The job of the compositor is to combine widgets in to a single view.
|
||||
|
||||
We do this because the terminal itself has no notion of overlapping windows in the way a desktop does.
|
||||
If an app wants to display overlapping components it must combine them into a single update.
|
||||
|
||||
Here's a video I generated over a year ago, demonstrating the output of the compositor:
|
||||
|
||||
<div class="video-wrapper">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
### The algorithm
|
||||
|
||||
You could be forgiven in thinking that the terminal is regular grid of characters and we can treat it like a 2D array.
|
||||
If that were the case, we could use [painter's algorithm](https://en.wikipedia.org/wiki/Painter's_algorithm) to handle the overlapping widgets.
|
||||
In other words, sort them back to front and render them as though they were bitmaps.
|
||||
|
||||
Unfortunately the terminal is *not* a true grid.
|
||||
Some characters such as CJK (Chinese, Japanese, and Korean) and many emoji are double the width of latin alphabet characters — which complicates things (to put it mildly).
|
||||
|
||||
Textual's way of handling this is inherited from [Rich](https://github.com/Textualize/rich).
|
||||
Anything you print in Rich, first generates a list of [Segments](https://github.com/Textualize/rich/blob/master/rich/segment.py) which consist of a string and associated style.
|
||||
These Segments are only converted into text with [ansi escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code) at the very last moment.
|
||||
|
||||
Textual works with same Segment object.
|
||||
Widgets all produce a list of segments, which are further processed by the compositor.
|
||||
|
||||
!!! tip "Switch the Primitive"
|
||||
|
||||
I hope I've earned the opertunity to dispense unsolicited pearls of wisdom.
|
||||
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.
|
||||
I call this "switching the primitive".
|
||||
|
||||
### Thinking in Segments
|
||||
|
||||
In the following illustration we have an app with three widgets; the "screen" (in blue) plus two floating widgets (in red and green).
|
||||
There will be many more widgets in a typical app, but this is enough to show how it works.
|
||||
|
||||
|
||||
<div class="excalidraw">
|
||||
--8<-- "docs/blog/images/compositor/widgets.excalidraw.svg"
|
||||
</div>
|
||||
|
||||
The lines are lists of Segments produced by the widget renderer.
|
||||
The compositor will combine those lists in to a single list where nothing overlaps.
|
||||
|
||||
To illustrate how this process works, let's consider the highlighted line about a quarter of the way down.
|
||||
|
||||
|
||||
### Compositing a line
|
||||
|
||||
Imagine you could view the terminal and widgets side on, so that you see a cross section of the terminal and the floating widgets.
|
||||
It would appear something like the following:
|
||||
|
||||
<div class="excalidraw">
|
||||
--8<-- "docs/blog/images/compositor/cuts0.excalidraw.svg"
|
||||
</div>
|
||||
|
||||
Those lines are produced when the widget is rendered and consist of a list of segments.
|
||||
|
||||
We want to combine these lists into a single list that covers the width of the terminal.
|
||||
That way we can update everything in a single write.
|
||||
|
||||
|
||||
### Step 1. Finding the cuts.
|
||||
|
||||
First thing the compositor does is to find every offset where a list of segments begins or ends.
|
||||
|
||||
<div class="excalidraw">
|
||||
--8<-- "docs/blog/images/compositor/cuts1.excalidraw.svg"
|
||||
</div>
|
||||
|
||||
### Step 2. Applying the cuts.
|
||||
|
||||
The next step is to divide every list of segments at the cut offsets.
|
||||
This will produce smaller lists of segments, which in the compositor code we refer to as *chops*.
|
||||
|
||||
<div class="excalidraw">
|
||||
--8<-- "docs/blog/images/compositor/cuts2.excalidraw.svg"
|
||||
</div>
|
||||
|
||||
|
||||
### Step 3. Discard chops.
|
||||
|
||||
Only the top-most chops will actually be visible to the viewer, so we can discard any chop that isn't at the top.
|
||||
|
||||
<div class="excalidraw">
|
||||
--8<-- "docs/blog/images/compositor/cuts3.excalidraw.svg"
|
||||
</div>
|
||||
|
||||
### Step 4. Combine.
|
||||
|
||||
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.
|
||||
|
||||
<div class="excalidraw">
|
||||
--8<-- "docs/blog/images/compositor/cuts4.excalidraw.svg"
|
||||
</div>
|
||||
|
||||
### What I omitted
|
||||
|
||||
There is more going on than this explanation may suggest.
|
||||
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.
|
||||
|
||||
Additionally, the compositor can do partial updates.
|
||||
In other words, if you click a button and it changes color the compositor can update just that button.
|
||||
|
||||
The compositor does all of this fast enough to enable smooth scrolling, even with a metric tonne of widgets on screen.
|
||||
Surprising fast, given it is essentially number crunching, which isn't considered Python's forté.
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
src: url("https://unpkg.com/@excalidraw/excalidraw@0.12.0/dist/excalidraw-assets/Cascadia.woff2");
|
||||
}
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
<script lang="js">
|
||||
|
||||
@@ -20,6 +20,7 @@ h3 .doc-heading code {
|
||||
body[data-md-color-primary="black"] .excalidraw svg {
|
||||
will-change: filter;
|
||||
filter: invert(100%) hue-rotate(180deg);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body[data-md-color-primary="black"] .excalidraw svg rect {
|
||||
|
||||