mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Add blog post about the placeholder PR.
This commit is contained in:
165
docs/blog/images/placeholder-example.svg
Normal file
165
docs/blog/images/placeholder-example.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 30 KiB |
233
docs/blog/posts/placeholder-pr.md
Normal file
233
docs/blog/posts/placeholder-pr.md
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
---
|
||||||
|
draft: false
|
||||||
|
date: 2022-11-22
|
||||||
|
categories:
|
||||||
|
- DevLog
|
||||||
|
authors:
|
||||||
|
- rodrigo
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
# What I learned from my first non-trivial PR
|
||||||
|
|
||||||
|
<div>
|
||||||
|
--8<-- "docs/blog/images/placeholder-example.svg"
|
||||||
|
</div>
|
||||||
|
|
||||||
|
It's 8:59 am and, by my Portuguese standards, it is freezing cold outside: 5 or 6 degrees Celsius.
|
||||||
|
It is my second day at Textualize and I just got into the office.
|
||||||
|
I undress my many layers of clothing to protect me from the Scottish cold and I sit down in my improvised corner of the Textualize office.
|
||||||
|
As I sit down, I turn myself in my chair to face my boss and colleagues to ask “So, what should I do today?”.
|
||||||
|
I was not expecting Will's answer, but the challenge excited me:
|
||||||
|
|
||||||
|
<!-- more -->
|
||||||
|
|
||||||
|
> “I thought I'll just throw you in the deep end and have you write some code.”
|
||||||
|
|
||||||
|
What happened next was that I spent two days [working on PR #1229](https://github.com/Textualize/textual/pull/1229) to add a new widget to the [Textual](https://github.com/Textualize/textual) code base.
|
||||||
|
At the time of writing, the pull request has not been merged yet.
|
||||||
|
Well, to be honest with you, it hasn't even been reviewed by anyone...
|
||||||
|
But that won't stop me from blogging about some of the things I learned while creating this PR.
|
||||||
|
|
||||||
|
|
||||||
|
## The placeholder widget
|
||||||
|
|
||||||
|
This PR adds a widget called `Placeholder` to Textual.
|
||||||
|
As per the documentation, this widget “is meant to have no complex functionality.
|
||||||
|
Use the placeholder widget when studying the layout of your app before having to develop your custom widgets.”
|
||||||
|
|
||||||
|
The point of the placeholder widget is that you can focus on building the layout of your app without having to have all of your (custom) widgets ready.
|
||||||
|
The placeholder widget also displays a couple of useful pieces of information to help you work out the layout of your app, namely the ID of the widget itself (or a custom label, if you provide one) and the width and height of the widget.
|
||||||
|
|
||||||
|
As an example of usage of the placeholder widget, you can refer to the screenshot at the top of this blog post, which I included below so you don't have to scroll up:
|
||||||
|
|
||||||
|
<div>
|
||||||
|
--8<-- "docs/blog/images/placeholder-example.svg"
|
||||||
|
</div>
|
||||||
|
|
||||||
|
The top left and top right widgets have custom labels.
|
||||||
|
Immediately under the top right placeholder, you can see some placeholders identified as `#p3`, `#p4`, and `#p5`.
|
||||||
|
Those are the IDs of the respective placeholders.
|
||||||
|
Then, rows 2 and 3 contain some placeholders that show their respective size and some placeholders that just contain some text.
|
||||||
|
|
||||||
|
|
||||||
|
## Bootstrapping the code for the widget
|
||||||
|
|
||||||
|
So, how does a code monkey start working on a non-trivial PR within 24 hours of joining a company?
|
||||||
|
The answer is simple: just copy and paste code!
|
||||||
|
But instead of copying and pasting from Stack Overflow, I decided to copy and paste from the internal code base.
|
||||||
|
|
||||||
|
My task was to create a new widget, so I thought it would be a good idea to take a look at the implementation of other Textual widgets.
|
||||||
|
For some reason I cannot seem to recall, I decided to take a look at the implementation of the button widget that you can find in [_button.py](https://github.com/Textualize/textual/blob/main/src/textual/widgets/_button.py).
|
||||||
|
By looking at how the button widget is implemented, I could immediately learn a few useful things about what I needed to do and some other things about how Textual works.
|
||||||
|
|
||||||
|
For example, a widget can have a class attribute called `DEFAULT_CSS` that specifies the default CSS for that widget.
|
||||||
|
I learned this just from staring at the code for the button widget.
|
||||||
|
|
||||||
|
Studying the code base will also reveal the standards that are in place.
|
||||||
|
For example, I learned that for a widget with variants (like the button with its “success” and “error” variants), the widget gets a CSS class with the name of the variant prefixed by a dash.
|
||||||
|
You can learn this by looking at the method `Button.watch_variant`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Button(Static, can_focus=True):
|
||||||
|
# ...
|
||||||
|
|
||||||
|
def watch_variant(self, old_variant: str, variant: str):
|
||||||
|
self.remove_class(f"-{old_variant}")
|
||||||
|
self.add_class(f"-{variant}")
|
||||||
|
```
|
||||||
|
|
||||||
|
In short, looking at code and files that are related to the things you need to do is a great way to get information about things you didn't even know you needed.
|
||||||
|
|
||||||
|
|
||||||
|
## Handling the placeholder variant
|
||||||
|
|
||||||
|
A button widget can have a different variant, which is mostly used by Textual to determine the CSS that should apply to the given button.
|
||||||
|
For the placeholder widget, we want the variant to determine what information the placeholder shows.
|
||||||
|
The [original GitHub issue](https://github.com/Textualize/textual/issues/1200) mentions 5 variants for the placeholder:
|
||||||
|
|
||||||
|
- a variant that just shows a label or the placeholder ID;
|
||||||
|
- a variant that shows the size and location of the placeholder;
|
||||||
|
- a variant that shows the state of the placeholder (does it have focus? is the mouse over it?);
|
||||||
|
- a variant that shows the CSS that is applied to the placeholder itself; and
|
||||||
|
- a variant that shows some text inside the placeholder.
|
||||||
|
|
||||||
|
The variant can be assigned when the placeholder is first instantiated, for example, `Placeholder("css")` would create a placeholder that shows its own CSS.
|
||||||
|
However, we also want to have an `on_click` handler that cycles through all the possible variants.
|
||||||
|
I was getting ready to reinvent the wheel when I remembered that the standard module [`itertools`](https://docs.python.org/3/library/itertools) has a lovely tool that does exactly what I needed!
|
||||||
|
Thus, all I needed to do was create a new `cycle` through the variants each time a placeholder is created and then grab the next variant whenever the placeholder is clicked:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Placeholder(Static):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
variant: PlaceholderVariant = "default",
|
||||||
|
*,
|
||||||
|
label: str | None = None,
|
||||||
|
name: str | None = None,
|
||||||
|
id: str | None = None,
|
||||||
|
classes: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
# ...
|
||||||
|
|
||||||
|
self.variant = self.validate_variant(variant)
|
||||||
|
# Set a cycle through the variants with the correct starting point.
|
||||||
|
self._variants_cycle = cycle(_VALID_PLACEHOLDER_VARIANTS_ORDERED)
|
||||||
|
while next(self._variants_cycle) != self.variant:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_click(self) -> None:
|
||||||
|
"""Click handler to cycle through the placeholder variants."""
|
||||||
|
self.cycle_variant()
|
||||||
|
|
||||||
|
def cycle_variant(self) -> None:
|
||||||
|
"""Get the next variant in the cycle."""
|
||||||
|
self.variant = next(self._variants_cycle)
|
||||||
|
```
|
||||||
|
|
||||||
|
I am just happy that I had the insight to add this little `while` loop when a placeholder is instantiated:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from itertools import cycle
|
||||||
|
# ...
|
||||||
|
class Placeholder(Static):
|
||||||
|
# ...
|
||||||
|
def __init__(...):
|
||||||
|
# ...
|
||||||
|
self._variants_cycle = cycle(_VALID_PLACEHOLDER_VARIANTS_ORDERED)
|
||||||
|
while next(self._variants_cycle) != self.variant:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
Can you see what would be wrong if this loop wasn't there?
|
||||||
|
|
||||||
|
|
||||||
|
## Updating the render of the placeholder on variant change
|
||||||
|
|
||||||
|
If the variant of the placeholder is supposed to determine what information the placeholder shows, then that information must be updated every time the variant of the placeholder changes.
|
||||||
|
Thankfully, Textual has reactive attributes and watcher methods, so all I needed to do was...
|
||||||
|
Defer the problem to another method:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Placeholder(Static):
|
||||||
|
# ...
|
||||||
|
variant = reactive("default")
|
||||||
|
# ...
|
||||||
|
def watch_variant(
|
||||||
|
self, old_variant: PlaceholderVariant, variant: PlaceholderVariant
|
||||||
|
) -> None:
|
||||||
|
self.validate_variant(variant)
|
||||||
|
self.remove_class(f"-{old_variant}")
|
||||||
|
self.add_class(f"-{variant}")
|
||||||
|
self.call_variant_update() # <-- let this method do the heavy lifting!
|
||||||
|
```
|
||||||
|
|
||||||
|
Doing this properly required some thinking.
|
||||||
|
Not that the current proposed solution is the best possible, but I did think of worse alternatives while I was thinking how to tackle this.
|
||||||
|
I wasn't entirely sure how I would manage the variant-dependant rendering because I am not a fan of huge conditional statements that look like switch statements:
|
||||||
|
|
||||||
|
```py
|
||||||
|
if variant == "default":
|
||||||
|
# render the default placeholder
|
||||||
|
elif variant == "size":
|
||||||
|
# render the placeholder with its size
|
||||||
|
elif variant == "state":
|
||||||
|
# render the state of the placeholder
|
||||||
|
elif variant == "css":
|
||||||
|
# render the placeholder with its CSS rules
|
||||||
|
elif variant == "text":
|
||||||
|
# render the placeholder with some text inside
|
||||||
|
```
|
||||||
|
|
||||||
|
However, I am a fan of using the built-in `getattr` and I thought of creating a rendering method for each different variant.
|
||||||
|
Then, all I needed to do was make sure the variant is part of the name of the method so that I can programmatically determine the name of the method that I need to call.
|
||||||
|
This means that the method `Placeholder.call_variant_update` is just this:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Placeholder(Static):
|
||||||
|
# ...
|
||||||
|
def call_variant_update(self) -> None:
|
||||||
|
"""Calls the appropriate method to update the render of the placeholder."""
|
||||||
|
update_variant_method = getattr(self, f"_update_{self.variant}_variant")
|
||||||
|
update_variant_method()
|
||||||
|
```
|
||||||
|
|
||||||
|
If `self.variant` is, say, `"size"`, then `update_variant_method` refers to `_update_size_variant`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Placeholder(Static):
|
||||||
|
# ...
|
||||||
|
def _update_size_variant(self) -> None:
|
||||||
|
"""Update the placeholder with the size of the placeholder."""
|
||||||
|
width, height = self.size
|
||||||
|
self._placeholder_label.update(f"[b]{width} x {height}[/b]")
|
||||||
|
```
|
||||||
|
|
||||||
|
This variant `"size"` also interacts with resizing events, so we have to watch out for those:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Placeholder(Static):
|
||||||
|
# ...
|
||||||
|
def on_resize(self, event: events.Resize) -> None:
|
||||||
|
"""Update the placeholder "size" variant with the new placeholder size."""
|
||||||
|
if self.variant == "size":
|
||||||
|
self._update_size_variant()
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Deleting code is a (hurtful) blessing
|
||||||
|
|
||||||
|
To conclude this blog post, let me muse about the fact that the original issue mentioned five placeholder variants and that my PR only includes two and a half.
|
||||||
|
|
||||||
|
After careful consideration and after coming up with the `getattr` mechanism to update the display of the placeholder according to the active variant, I started showing the “final” product to Will and my other colleagues.
|
||||||
|
Eventually, we ended up getting rid of the variant for CSS and the variant that shows the placeholder state.
|
||||||
|
This means that I had to **delete part of my code** even before it saw the light of day.
|
||||||
|
|
||||||
|
On the one hand, deleting those chunks of code made me a bit sad.
|
||||||
|
After all, I had spent quite some time thinking about how to best implement that functionality!
|
||||||
|
But then, it was time to write documentation and tests, and I verified that the **best code** is the code that you don't even write!
|
||||||
|
The code you don't write is guaranteed to have zero bugs and it also does not need any documentation whatsoever!
|
||||||
|
|
||||||
|
So, it was a shame that some lines of code I poured my heart and keyboard into did not get merged into the Textual code base.
|
||||||
|
On the other hand, I am quite grateful that I won't have to fix the bugs that will certainly reveal themselves in a couple of weeks or months from now.
|
||||||
|
Heck, the code hasn't been merged yet and just by writing this blog post I noticed a couple of tweaks that were missing!
|
||||||
Reference in New Issue
Block a user