Merge branch 'main' of github.com:Textualize/textual into datatable-events

This commit is contained in:
Darren Burns
2023-01-16 11:00:10 +00:00
243 changed files with 12764 additions and 1680 deletions

View File

@@ -12,5 +12,4 @@ repos:
rev: 22.10.0
hooks:
- id: black
exclude: ^tests/
exclude: ^tests/snapshot_tests

View File

@@ -12,15 +12,27 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Added `TreeNode.parent` -- a read-only property for accessing a node's parent https://github.com/Textualize/textual/issues/1397
- Added public `TreeNode` label access via `TreeNode.label` https://github.com/Textualize/textual/issues/1396
- Added read-only public access to the children of a `TreeNode` via `TreeNode.children` https://github.com/Textualize/textual/issues/1398
- Added `Tree.get_node_by_id` to allow getting a node by its ID https://github.com/Textualize/textual/pull/1535
- Added a `Tree.NodeHighlighted` message, giving a `on_tree_node_highlighted` event handler https://github.com/Textualize/textual/issues/1400
- Added a `inherit_component_classes` subclassing parameter to control whether or not component classes are inherited from base classes https://github.com/Textualize/textual/issues/1399
- Added `diagnose` as a `textual` command https://github.com/Textualize/textual/issues/1542
### Changed
- `MouseScrollUp` and `MouseScrollDown` now inherit from `MouseEvent` and have attached modifier keys. https://github.com/Textualize/textual/pull/1458
- Fail-fast and print pretty tracebacks for Widget compose errors https://github.com/Textualize/textual/pull/1505
- Added Widget._refresh_scroll to avoid expensive layout when scrolling https://github.com/Textualize/textual/pull/1524
- `events.Paste` now bubbles https://github.com/Textualize/textual/issues/1434
- Clock color in the `Header` widget now matches the header color https://github.com/Textualize/textual/issues/1459
- `COMPONENT_CLASSES` are now inherited from base classes https://github.com/Textualize/textual/issues/1399
- Watch methods may now take no parameters
- Added `compute` parameter to reactive
### Fixed
- The styles `scrollbar-background-active` and `scrollbar-color-hover` are no longer ignored https://github.com/Textualize/textual/pull/1480
- The widget `Placeholder` can now have its width set to `auto` https://github.com/Textualize/textual/pull/1508
- Behavior of widget `Input` when rendering after programmatic value change and related scenarios https://github.com/Textualize/textual/issues/1477 https://github.com/Textualize/textual/issues/1443
## [0.9.1] - 2022-12-30

View File

@@ -0,0 +1,323 @@
---
draft: false
date: 2023-01-09
categories:
- DevLog
authors:
- davep
---
# So you're looking for a wee bit of Textual help...
## Introduction
!!! quote
Patience, Highlander. You have done well. But it'll take time. You are
generations being born and dying. You are at one with all living things.
Each man's thoughts and dreams are yours to know. You have power beyond
imagination. Use it well, my friend. Don't lose your head.
<cite>Juan Sánchez Villalobos Ramírez, Chief metallurgist to King Charles V of Spain</cite>
As of the time of writing, I'm a couple or so days off having been with
Textualize for 3 months. It's been fun, and educational, and every bit as
engaging as I'd hoped, and more. One thing I hadn't quite prepared for
though, but which I really love, is how so many other people are learning
Textual along with me.
<!-- more -->
Even in those three months the library has changed and expanded quite a lot,
and it continues to do so. Meanwhile, more people are turning up and using
the framework; you can see this online in social media, blogs and of course
[in the ever-growing list of projects on GitHub which depend on
Textual](https://github.com/Textualize/textual/network/dependents).
This inevitably means there's a lot of people getting to grips with a new
tool, and one that is still a bit of a moving target. This in turn means
lots of people are coming to us to get help.
As I've watched this happen I've noticed a few patterns emerging. Some of
these good or neutral, some... let's just say not really beneficial to those
seeking the help, or to those trying to provide the help. So I wanted to
write a little bit about the different ways you can get help with Textual
and your Textual-based projects, and to also try and encourage people to
take the most helpful and positive approach to getting that help.
Now, before I go on, I want to make something *very* clear: I'm writing this
as an individual. This is my own personal view, and my own advice from me to
anyone who wishes to take it. It's not Textual (the project) or Textualize
(the company) policy, rules or guidelines. This is just some ageing hacker's
take on how best to go about asking for help, informed by years of asking
for and also providing help in email, on Usenet, on forums, etc.
Or, put another way: if what you read in here seems sensible to you, I
figure we'll likely have already hit it off [over on
GitHub](https://github.com/Textualize/textual) or in [the Discord
server](https://discord.gg/Enf6Z3qhVr). ;-)
## Where to go for help
At this point this is almost a bit of an FAQ itself, so I thought I'd
address it here: where's the best place to ask for help about Textual, and
what's the difference between GitHub Issues, Discussions and our Discord
server?
I'd suggest thinking of them like this:
### Discord
You have a question, or need help with something, and perhaps you could do
with a reply as soon as possible. But, and this is the **really important
part**, it doesn't matter if you don't get a response. If you're in this
situation then the Discord server is possibly a good place to start. If
you're lucky someone will be hanging about who can help out.
I can't speak for anyone else, but keep this in mind: when I look in on
Discord I tend not to go scrolling back much to see if anything has been
missed. If something catches my eye, I'll try and reply, but if it
doesn't... well, it's mostly an instant chat thing so I don't dive too
deeply back in time.
!!! tip inline end "Going from Discord to a GitHub issue"
As a slight aside here: sometimes people will pop up in Discord, ask a
question about something that turns out looking like a bug, and that's
the last we hear of it. Please, please, **please**, if this happens, the
most helpful thing you can do is go raise an issue for us. It'll help us
to keep track of problems, it'll help get your problem fixed, it'll mean
everyone benefits.
My own advice would be to treat Discord as an ephemeral resource. It happens
in the moment but fades away pretty quickly. It's like knocking on a
friend's door to see if they're in. If they're not in, you might leave them
a note, which is sort of like going to...
### GitHub
On the other hand, if you have a question or need some help or something
where you want to stand a good chance of the Textual developers (amongst
others) seeing it and responding, I'd recommend that GitHub is the place to
go. Dropping something into the discussions there, or leaving an issue,
ensures it'll get seen. It won't get lost.
As for which you should use -- a discussion or an issue -- I'd suggest this:
if you need help with something, or you want to check your understanding of
something, or you just want to be sure something is a problem before taking
it further, a discussion might be the best thing. On the other hand, if
you've got a clear bug or feature request on your hands, an issue makes a
lot of sense.
Don't worry if you're not sure which camp your question or whatever falls
into though; go with what you think is right. There's no harm done either
way (I may move an issue to a discussion first before replying, if it's
really just a request for help -- but that's mostly so everyone can benefit
from finding it in the right place later on down the line).
## The dos and don'ts of getting help
Now on to the fun part. This is where I get a bit preachy. Ish. Kinda. A
little bit. Again, please remember, this isn't a set of rules, this isn't a
set of official guidelines, this is just a bunch of *"if you want my advice,
and I know you didn't ask but you've read this far so you actually sort of
did don't say I didn't warn you!"* waffle.
This isn't going to be an exhaustive collection, far from it. But I feel
these are some important highlights.
### Do...
When looking for help, in any of the locations mentioned above, I'd totally
encourage:
#### Be clear and detailed
Too much detail is almost always way better than not enough. *"My program
didn't run"*, often even with some of the code supplied, is so much harder
to help than *"I ran this code I'm posting here, and I expected this
particular outcome, and I expected it because I'd read this particular thing
in the docs and had comprehended it to mean this, but instead the outcome
was this exception here, and I'm a bit stuck -- can someone offer some
pointers?"*
The former approach means there often ends up having to be a back and forth
which can last a long time, and which can sometimes be frustrating for the
person asking. Manage frustration: be clear, tell us everything you can.
#### Say what resources you've used already
If you've read the potions of the documentation that relate to what you're
trying to do, it's going to be really helpful if you say so. If you don't,
it might be assumed you haven't and you may end up being pointed at them.
So, please, if you've checked the documentation, looked in the FAQ, done a
search of past issues or discussions or perhaps even done a search on the
Discord server... please say so.
#### Be polite
This one can go a long way when looking for help. Look, I get it,
programming is bloody frustrating at times. We've all rage-quit some code at
some point, I'm sure. It's likely going to be your moment of greatest
frustration when you go looking for help. But if you turn up looking for
help acting all grumpy and stuff it's not going to come over well. Folk are
less likely to be motivated to lend a hand to someone who seems rather
annoyed.
If you throw in a please and thank-you here and there that makes it all the
better.
#### Fully consider the replies
You could find yourself getting a reply that you're sure won't help at all.
That's fair. But be sure to fully consider it first. Perhaps you missed the
obvious along the way and this is 100% the course correction you'd
unknowingly come looking for in the first place. Sure, the person replying
might have totally misunderstood what was being asked, or might be giving a
wrong answer (it me! I've totally done that and will again!), but even then
a reply along the lines of *"I'm not sure that's what I'm looking for,
because..."* gets everyone to the solution faster than *"lol nah"*.
#### Entertain what might seem like odd questions
Aye, I get it, being asked questions when you're looking for an *answer* can
be a bit frustrating. But if you find yourself on the receiving end of a
small series of questions about your question, keep this in mind: Textual is
still rather new and still developing and it's possible that what you're
trying to do isn't the correct way to do that thing. To the person looking
to help you it may seem to them you have an [XY
problem](https://en.wikipedia.org/wiki/XY_problem).
Entertaining those questions might just get you to the real solution to your
problem.
#### Allow for language differences
You don't need me to tell you that a project such as Textual has a global
audience. With that rather obvious fact comes the other fact that we don't
all share the same first language. So, please, as much as possible, try and
allow for that. If someone is trying to help you out, and they make it clear
they're struggling to follow you, keep this in mind.
#### Acknowledge the answer
I suppose this is a variation on "be polite" (really, a thanks can go a long
way), but there's more to this than a friendly acknowledgement. If someone
has gone to the trouble of offering some help, it's helpful to everyone who
comes after you to acknowledge if it worked or not. That way a future
help-seeker will know if the answer they're reading stands a chance of being
the right one.
#### Accept that Textual is zero-point software (right now)
Of course the aim is to have every release of Textual be stable and useful,
but things will break. So, please, do keep in mind things like:
- Textual likely doesn't have your feature of choice just yet.
- We might accidentally break something (perhaps pinning Textual and testing
each release is a good plan here?).
- We might deliberately break something because we've decided to take a
particular feature or way of doing things in a better direction.
Of course it can be a bit frustrating a times, but overall the aim is to
have the best framework possible in the long run.
### Don't...
Okay, now for a bit of old-hacker finger-wagging. Here's a few things I'd
personally discourage:
#### Lack patience
Sure, it can be annoying. You're in your flow, you've got a neat idea for a
thing you want to build, you're stuck on one particular thing and you really
need help right now! Thing is, that's unlikely to happen. Badgering
individuals, or a whole resource, to reply right now, or complaining that
it's been `$TIME_PERIOD` since you asked and nobody has replied... that's
just going to make people less likely to reply.
#### Unnecessarily tag individuals
This one often goes hand in hand with the "lack patience" thing: Be it
asking on Discord, or in GitHub issues, discussions or even PRs,
unnecessarily tagging individuals is a bit rude. Speaking for myself and
only myself: I *love* helping folk with Textual. If I could help everyone
all the time the moment they have a problem, I would. But it doesn't work
like that. There's any number of reasons I might not be responding to a
particular request, including but not limited to (here I'm talking
personally because I don't want to speak for anyone else, but I'm sure I'm
not alone here):
- I have a job. Sure, my job is (in part) Textual, but there's more to it
than that particular issue. I might be doing other stuff.
- I have my own projects to work on too. I like coding for fun as well (or
writing preaching old dude blog posts like this I guess, but you get the
idea).
- I actually have other interests outside of work hours so I might actually
be out doing a 10k in the local glen, or battling headcrabs in VR, or
something.
- Housework. :-/
You get the idea though. So while I'm off having a well-rounded life, it's
not good to get unnecessarily intrusive alerts to something that either a)
doesn't actually directly involve me or b) could wait.
#### Seek personal support
Again, I'm going to speak totally for myself here, but I also feel the
general case is polite for all: there's a lot of good support resources
available already; sending DMs on Discord or Twitter or in the Fediverse,
looking for direct personal support, isn't really the best way to get help.
Using the public/collective resources is absolutely the *best* way to get
that help. Why's it a bad idea to dive into DMs? Here's some reasons I think
it's not a good idea:
- It's a variation on "unnecessarily tagging individuals".
- You're short-changing yourself when it comes to getting help. If you ask
somewhere more public you're asking a much bigger audience, who
collectively have more time, more knowledge and more experience than a
single individual.
- Following on from that, any answers can be (politely) fact-checked or
enhanced by that audience, resulting in a better chance of getting the
best help possible.
- The next seeker-of-help gets to miss out on your question and the answer.
If asked and answered in public, it's a record that can help someone else
in the future.
#### Doubt your ability or skill level
I suppose this should really be phrased as a do rather than a don't, as here
I want to encourage something positive. A few times I've helped people out
who have been very apologetic about their questions being "noob" questions,
or about how they're fairly new to Python, or programming in general.
Really, please, don't feel the need to apologise and don't be ashamed of
where you're at.
If you've asked something that's obviously answered in the documentation,
that's not a problem; you'll likely get pointed at the docs and it's what
happens next that's the key bit. If the attitude is *"oh, cool, that's
exactly what I needed to be reading, thanks!"* that's a really positive
thing. The only time it's a problem is when there's a real reluctance to use
the available resources. We've all seen that person somewhere at some point,
right? ;-)
Not knowing things [is totally cool](https://xkcd.com/1053/).
## Conclusion
So, that's my waffle over. As I said at the start: this is my own personal
thoughts on how to get help with Textual, both as someone whose job it is to
work on Textual and help people with Textual, and also as a FOSS advocate
and supporter who can normally be found helping Textual users when he's not
"on the clock" too.
What I've written here isn't exhaustive. Neither is it novel. Plenty has
been written on the general subject in the past, and I'm sure more will be
written on the subject in the future. I do, however, feel that these are the
most common things I notice. I'd say those dos and don'ts cover 90% of *"can
I get some help?"* interactions; perhaps closer to 99%.
Finally, and I think this is the most important thing to remember, the next
time you are battling some issue while working with Textual: [don't lose
your head](https://www.youtube.com/watch?v=KdYvKF9O7Y8)!

View File

@@ -396,7 +396,7 @@ Below you can see the code I wrote and a short animation of the app working.
=== "CSS"
```css
```sass
Screen {
align: center middle;
}

View File

@@ -0,0 +1,70 @@
<!-- Template file for a Textual CSS type reference page. -->
# &lt;type-name&gt;
<!-- Short description of the type. -->
## Syntax
<!--
For a simple type like <integer>:
Describe the type in a short paragraph with an absolute link to the type page.
E.g., “The [`<my-type>`](/css_types/my_type) type is such and such with sprinkles on top.”
-->
<!--
For a type with many different values like <color>:
Introduce the type with a link to [`<my-type>`](/css_types/my_type).
Then, a bullet list with the variants accepted:
- you can create this type with X Y Z;
- you can also do A B C; and
- also use D E F.
-->
<!--
For a type that accepts specific options like <border>:
Add a sentence and a table. Consider ordering values in alphabetical order if there is no other obvious ordering. See below:
The [`<my-type>`](/css_types/my_type) type can take any of the following values:
| Value | Description |
|---------------|-----------------------------------------------|
| `abc` | Describe here. |
| `other val` | Describe this one also. |
| `value three` | Please use full stops. |
| `zyx` | Describe the value without assuming any rule. |
-->
## Examples
### CSS
<!--
Include a good variety of examples.
If the type has many different syntaxes, cover all of them.
Add comments when needed/if helpful.
-->
```sass
.some-class {
rule: type-value-1;
rule: type-value-2;
rule: type-value-3;
}
```
### Python
<!-- Same examples as above. -->
```py
widget.styles.rule = type_value_1
widget.styles.rule = type_value_2
widget.styles.rule = type_value_3
```

54
docs/css_types/border.md Normal file
View File

@@ -0,0 +1,54 @@
# &lt;border&gt;
The `<border>` CSS type represents a border style.
## Syntax
The [`<border>`](/css_types/border) type can take any of the following values:
| Border type | Description |
|-------------|----------------------------------------------------------|
| `ascii` | A border with plus, hyphen, and vertical bar characters. |
| `blank` | A blank border (reserves space for a border). |
| `dashed` | Dashed line border. |
| `double` | Double lined border. |
| `heavy` | Heavy border. |
| `hidden` | Alias for "none". |
| `hkey` | Horizontal key-line border. |
| `inner` | Thick solid border. |
| `none` | Disabled border. |
| `outer` | Solid border with additional space around content. |
| `round` | Rounded corners. |
| `solid` | Solid border. |
| `tall` | Solid border with additional space top and bottom. |
| `vkey` | Vertical key-line border. |
| `wide` | Solid border with additional space left and right. |
## Border command
The `textual` CLI has a subcommand which will let you explore the various border types interactively, when applied to the CSS rule [`border`](../styles/border.md):
```
textual borders
```
## Examples
### CSS
```sass
#container {
border: heavy red;
}
#heading {
border-bottom: solid blue;
}
```
### Python
```py
widget.styles.border = ("heavy", "red")
widget.styles.border_bottom = ("solid", "blue")
```

138
docs/css_types/color.md Normal file
View File

@@ -0,0 +1,138 @@
# &lt;color&gt;
The `<color>` CSS type represents a color.
!!! warning
Not to be confused with the [`color`](../styles/color.md) CSS rule to set text color.
## Syntax
A [`<color>`](/css_types/color) should be in one of the formats explained in this section.
A bullet point summary of the formats available follows:
- a recognised [named color](#named-colors) (e.g., `red`);
- a 3 or 6 hexadecimal digit number representing the [RGB values](#hex-rgb-value) of the color (e.g., `#F35573`);
- a 4 or 8 hexadecimal digit number representing the [RGBA values](#hex-rgba-value) of the color (e.g., `#F35573A0`);
- a color description in the RGB system, [with](#rgba-description) or [without](#rgb-description) transparency (e.g., `rgb(23, 78, 200)`);
- a color description in the HSL system, [with](#hsla-description) or [without](#hsl-description) transparency (e.g., `hsl(290, 70%, 80%)`);
[Textual's default themes](../../guide/design#theme-reference) also provide many CSS variables with colors that can be used out of the box.
### Named colors
A named color is a [`<name>`](./name.md) that Textual recognises.
Below, you can find a (collapsed) list of all of the named colors that Textual recognises, along with their hexadecimal values, their RGB values, and a visual sample.
<details>
<summary>All named colors available.</summary>
```{.rich columns="80" title="colors"}
from textual._color_constants import COLOR_NAME_TO_RGB
from textual.color import Color
from rich.table import Table
from rich.text import Text
table = Table("Name", "hex", "RGB", "Color", expand=True, highlight=True)
for name, triplet in sorted(COLOR_NAME_TO_RGB.items()):
if len(triplet) != 3:
continue
color = Color(*triplet)
r, g, b = triplet
table.add_row(
f'"{name}"',
Text(f"{color.hex}", "bold green"),
f"rgb({r}, {g}, {b})",
Text(" ", style=f"on rgb({r},{g},{b})")
)
output = table
```
</details>
### Hex RGB value
The hexadecimal RGB format starts with an octothorpe `#` and is then followed by 3 or 6 hexadecimal digits: `0123456789ABCDEF`.
Casing is ignored.
- If 6 digits are used, the format is `#RRGGBB`:
- `RR` represents the red channel;
- `GG` represents the green channel; and
- `BB` represents the blue channel.
- If 3 digits are used, the format is `#RGB`.
In a 3 digit color, each channel is represented by a single digit which is duplicated when converting to the 6 digit format.
For example, the color `#A2F` is the same as `#AA22FF`.
### Hex RGBA value
This is the same as the [hex RGB value](#hex-rgb-value), but with an extra channel for the alpha component (that sets transparency).
- If 8 digits are used, the format is `#RRGGBBAA`, equivalent to the format `#RRGGBB` with two extra digits for transparency.
- If 4 digits are used, the format is `#RGBA`, equivalent to the format `#RGB` with an extra digit for transparency.
### `rgb` description
The `rgb` format description is a functional description of a color in the RGB color space.
This description follows the format `rgb(red, green, blue)`, where `red`, `green`, and `blue` are decimal integers between 0 and 255.
They represent the value of the channel with the same name.
For example, `rgb(0, 255, 32)` is equivalent to `#00FF20`.
### `rgba` description
The `rgba` format description is the same as the `rgb` with an extra parameter for transparency, which should be a value between `0` and `1`.
For example, `rgba(0, 255, 32, 0.5)` is the color `rgb(0, 255, 32)` with 50% transparency.
### `hsl` description
The `hsl` format description is a functional description of a color in the HSL color space.
This description follows the format `hsl(hue, saturation, lightness)`, where
- `hue` is a float between 0 and 360;
- `saturation` is a percentage between `0%` and `100%`; and
- `lightness` is a percentage between `0%` and `100%`.
For example, the color `#00FF20` would be represented as `hsl(128, 100%, 50%)` in the HSL color space.
### `hsla` description
The `hsla` format description is the same as the `hsl` with an extra parameter for transparency, which should be a value between `0` and `1`.
For example, `hsla(128, 100%, 50%, 0.5)` is the color `hsl(128, 100%, 50%)` with 50% transparency.
## Examples
### CSS
```sass
Header {
background: red; /* Color name */
}
.accent {
color: $accent; /* Textual variable */
}
#footer {
tint: hsl(300, 20%, 70%); /* HSL description */
}
```
### Python
In Python, rules that expect a `<color>` can also accept an instance of the type [`Color`][textual.color.Color].
```py
# Mimicking the CSS syntax
widget.styles.background = "red" # Color name
widget.styles.color = "$accent" # Textual variable
widget.styles.tint = "hsl(300, 20%, 70%)" # HSL description
from textual.color import Color
# Using a Color object directly...
color = Color(16, 200, 45)
# ... which can also parse the CSS syntax
color = Color.parse("#A8F")
```

View File

@@ -0,0 +1,29 @@
# &lt;horizontal&gt;
The `<horizontal>` CSS type represents a position along the horizontal axis.
## Syntax
The [`<horizontal>`](/css_types/horizontal) type can take any of the following values:
| Value | Description |
| ---------------- | -------------------------------------------- |
| `center` | Aligns in the center of the horizontal axis. |
| `left` (default) | Aligns on the left of the horizontal axis. |
| `right` | Aligns on the right of the horizontal axis. |
## Examples
### CSS
```sass
.container {
align-horizontal: right;
}
```
### Python
```py
widget.styles.align_horizontal = "right"
```

12
docs/css_types/index.md Normal file
View File

@@ -0,0 +1,12 @@
# CSS Types
CSS types define the values that Textual CSS styles accept.
CSS types will be linked from within the [styles reference](../styles/index.md) in the "Formal Syntax" section of each style.
The CSS types will be denoted by a keyword enclosed by angle brackets `<` and `>`.
For example, the style [`align-horizontal`](../styles/align.md) references the CSS type [`<horizontal>`](./horizontal.md):
--8<-- "docs/snippets/syntax_block_start.md"
align-horizontal: <a href="./horizontal.md">&lt;horizontal&gt;</a>;
--8<-- "docs/snippets/syntax_block_end.md"

29
docs/css_types/integer.md Normal file
View File

@@ -0,0 +1,29 @@
# &lt;integer&gt;
The `<integer>` CSS type represents an integer number.
## Syntax
An [`<integer>`](/css_types/integer) is any valid integer number like `-10` or `42`.
!!! note
Some CSS rules may expect an `<integer>` within certain bounds. If that is the case, it will be noted in that rule.
## Examples
### CSS
```sass
.classname {
offset: 10 -20
}
```
### Python
In Python, a rule that expects a CSS type `<integer>` will expect a value of the type `int`:
```py
widget.styles.offset = (10, -20)
```

26
docs/css_types/name.md Normal file
View File

@@ -0,0 +1,26 @@
# &lt;name&gt;
The `<name>` type represents a sequence of characters that identifies something.
## Syntax
A [`<name>`](/css_types/name) is any non-empty sequence of characters:
- starting with a letter `a-z`, `A-Z`, or underscore `_`; and
- followed by zero or more letters `a-zA-Z`, digits `0-9`, underscores `_`, and hiphens `-`.
## Examples
### CSS
```sass
Screen {
layers: onlyLetters Letters-and-hiphens _lead-under letters-1-digit;
}
```
### Python
```py
widget.styles.layers = "onlyLetters Letters-and-hiphens _lead-under letters-1-digit"
```

30
docs/css_types/number.md Normal file
View File

@@ -0,0 +1,30 @@
# &lt;number&gt;
The `<number>` CSS type represents a real number, which can be an integer or a number with a decimal part (akin to a `float` in Python).
## Syntax
A [`<number>`](/css_types/number) is an [`<integer>`](/css_types/integer), optionally followed by the decimal point `.` and a decimal part composed of one or more digits.
## Examples
### CSS
```sass
Grid {
grid-size: 3 6 /* Integers are numbers */
}
.translucid {
opacity: 0.5 /* Numbers can have a decimal part */
}
```
### Python
In Python, a rule that expects a CSS type `<number>` will accept an `int` or a `float`:
```py
widget.styles.grid_size = (3, 6) # Integers are numbers
widget.styles.opacity = 0.5 # Numbers can have a decimal part
```

View File

@@ -0,0 +1,29 @@
# &lt;overflow&gt;
The `<overflow>` CSS type represents overflow modes.
## Syntax
The [`<overflow>`](/css_types/overflow) type can take any of the following values:
| Value | Description |
|----------|----------------------------------------|
| `auto` | Determine overflow mode automatically. |
| `hidden` | Don't overflow. |
| `scroll` | Allow overflowing. |
## Examples
### CSS
```sass
#container {
overflow-y: hidden; /* Don't overflow */
}
```
### Python
```py
widget.styles.overflow_y = "hidden" # Don't overflow
```

View File

@@ -0,0 +1,37 @@
# &lt;percentage&gt;
The `<percentage>` CSS type represents a percentage value.
It is often used to represent values that are relative to the parent's values.
!!! warning
Not to be confused with the [`<scalar>`](./scalar.md) type.
## Syntax
A [`<percentage>`](/css_types/percentage) is a [`<number>`](/css_types/number) followed by the percent sign `%` (without spaces).
Some rules may clamp the values between `0%` and `100%`.
## Examples
### CSS
```sass
#footer {
/* Integer followed by % */
color: red 70%;
/* The number can be negative/decimal, although that may not make sense */
offset: -30% 12.5%;
}
```
### Python
```py
# Integer followed by %
widget.styles.color = "red 70%"
# The number can be negative/decimal, althought that may not make sense
widget.styles.offset = ("-30%", "12.5%")
```

113
docs/css_types/scalar.md Normal file
View File

@@ -0,0 +1,113 @@
# &lt;scalar&gt;
The `<scalar>` CSS type represents a length.
It can be a [`<number>`](./number.md) and a unit, or the special value `auto`.
It is used to represent lengths, for example in the [`width`](../styles/width.md) and [`height`](../styles/height.md) rules.
!!! warning
Not to be confused with the [`<number>`](./number.md) or [`<percentage>`](./percentage.md) types.
## Syntax
A [`<scalar>`](/css_types/scalar) can be any of the following:
- a fixed number of cells (e.g., `10`);
- a fractional proportion relative to the sizes of the other widgets (e.g., `1fr`);
- a percentage relative to the container widget (e.g., `50%`);
- a percentage relative to the container width/height (e.g., `25w`/`75h`);
- a percentage relative to the viewport width/height (e.g., `25vw`/`75vh`); or
- the special value `auto` to compute the optimal size to fit without scrolling.
A complete reference table and detailed explanations follow.
You can [skip to the examples](#examples).
| Unit symbol | Unit | Example | Description |
|-------------|-----------------|---------|-------------------------------------------------------------|
| `""` | Cell | `10` | Number of cells (rows or columns). |
| `"fr"` | Fraction | `1fr` | Specifies the proportion of space the widget should occupy. |
| `"%"` | Percent | `75%` | Length relative to the container widget. |
| `"w"` | Width | `25w` | Percentage relative to the width of the container widget. |
| `"h"` | Height | `75h` | Percentage relative to the height of the container widget. |
| `"vw"` | Viewport width | `25vw` | Percentage relative to the viewport width. |
| `"vh"` | Viewport height | `75vh` | Percentage relative to the viewport height. |
| - | Auto | `auto` | Tries to compute the optimal size to fit without scrolling. |
### Cell
The number of cells is the only unit for a scalar that is _absolute_.
This can be an integer or a float but floats are truncated to integers.
If used to specify a horizontal length, it corresponds to the number of columns.
For example, in `width: 15`, this sets the width of a widget to be equal to 15 cells, which translates to 15 columns.
If used to specify a vertical length, it corresponds to the number of lines.
For example, in `height: 10`, this sets the height of a widget to be equal to 10 cells, which translates to 10 lines.
### Fraction
The unit fraction is used to represent proportional sizes.
For example, if two widgets are side by side and one has `width: 1fr` and the other has `width: 3fr`, the second one will be three times as wide as the first one.
### Percent
The percent unit matches a [`<percentage>`](./percentage.md) and is used to specify a total length relative to the space made available by the container widget.
If used to specify a horizontal length, it will be relative to the width of the container.
For example, `width: 50%` sets the width of a widget to 50% of the width of its container.
If used to specify a vertical length, it will be relative to the height of the container.
For example, `height: 50%` sets the height of a widget to 50% of the height of its container.
### Width
The width unit is similar to the percent unit, except it sets the percentage to be relative to the width of the container.
For example, `width: 25w` sets the width of a widget to 25% of the width of its container and `height: 25w` sets the height of a widget to 25% of the width of its container.
So, if the container has a width of 100 cells, the width and the height of the child widget will be of 25 cells.
### Height
The height unit is similar to the percent unit, except it sets the percentage to be relative to the height of the container.
For example, `height: 75h` sets the height of a widget to 75% of the height of its container and `width: 75h` sets the width of a widget to 75% of the height of its container.
So, if the container has a height of 100 cells, the width and the height of the child widget will be of 75 cells.
### Viewport width
This is the same as the [width unit](#width), except that it is relative to the width of the viewport instead of the width of the immediate container.
The width of the viewport is the width of the terminal minus the widths of widgets that are docked left or right.
For example, `width: 25vw` will try to set the width of a widget to be 25% of the viewport width, regardless of the widths of its containers.
### Viewport height
This is the same as the [height unit](#height), except that it is relative to the height of the viewport instead of the height of the immediate container.
The height of the viewport is the height of the terminal minus the heights of widgets that are docked top or bottom.
For example, `height: 75vh` will try to set the height of a widget to be 75% of the viewport height, regardless of the height of its containers.
### Auto
This special value will try to calculate the optimal size to fit the contents of the widget without scrolling.
For example, if its container is big enough, a label with `width: auto` will be just as wide as its text.
## Examples
### CSS
```sass
Horizontal {
width: 60; /* 60 cells */
height: 1fr; /* proportional size of 1 */
}
```
### Python
```py
widget.styles.width = 16 # Cell unit can be specified with an int/float
widget.styles.height = "1fr" # proportional size of 1
```

View File

@@ -0,0 +1,40 @@
# &lt;text-align&gt;
The `<text-align>` CSS type represents alignments that can be applied to text.
!!! warning
Not to be confused with the [`text-align`](../styles/text_align.md) CSS rule that sets the alignment of text in a widget.
## Syntax
A [`<text-align>`](/css_types/text_align) can be any of the following values:
| Value | Alignment type |
|-----------|--------------------------------------|
| `center` | Center alignment. |
| `end` | Alias for `right`. |
| `justify` | Text is justified inside the widget. |
| `left` | Left alignment. |
| `right` | Right alignment. |
| `start` | Alias for `left`. |
!!! tip
The meanings of `start` and `end` will likely change when RTL languages become supported by Textual.
## Examples
### CSS
```sass
Label {
rule: justify;
}
```
### Python
```py
widget.styles.text_align = "justify"
```

View File

@@ -0,0 +1,46 @@
# &lt;text-style&gt;
The `<text-style>` CSS type represents styles that can be applied to text.
!!! warning
Not to be confused with the [`text-style`](../styles/text_style.md) CSS rule that sets the style of text in a widget.
## Syntax
A [`<text-style>`](/css_types/text_style) can be any _space-separated_ combination of the following values:
| Value | Description |
|-------------|-----------------------------------------------------------------|
| `bold` | **Bold text.** |
| `italic` | _Italic text._ |
| `none` | Plain text with no styling. |
| `reverse` | Reverse video text (foreground and background colors reversed). |
| `strike` | <s>Strikethrough text.</s> |
| `underline` | <u>Underline text.</u> |
## Examples
### CSS
```sass
#label1 {
/* You can specify any value by itself. */
rule: strike;
}
#label2 {
/* You can also combine multiple values. */
rule: strike bold italic reverse;
}
```
### Python
```py
# You can specify any value by itself
widget.styles.text_style = "strike"
# You can also combine multiple values
widget.styles.text_style = "bold underline italic"
```

View File

@@ -0,0 +1,29 @@
# &lt;vertical&gt;
The `<vertical>` CSS type represents a position along the vertical axis.
## Syntax
The [`<vertical>`](/css_types/vertical) type can take any of the following values:
| Value | Description |
| --------------- | ------------------------------------------ |
| `bottom` | Aligns at the bottom of the vertical axis. |
| `middle` | Aligns in the middle of the vertical axis. |
| `top` (default) | Aligns at the top of the vertical axis. |
## Examples
### CSS
```sass
.container {
align-vertical: top;
}
```
### Python
```py
widget.styles.align_vertical = "top"
```

View File

@@ -12,7 +12,7 @@ from textual.widgets import Static, Input
class DictionaryApp(App):
"""Searches ab dictionary API as-you-type."""
"""Searches a dictionary API as-you-type."""
CSS_PATH = "dictionary.css"

View File

@@ -11,8 +11,8 @@ Static {
}
#box1 {
background: darkcyan;
layer: above;
background: darkcyan;
}
#box2 {

View File

@@ -1,11 +1,11 @@
from textual.app import App
from textual.widgets import Static
from textual.widgets import Label
class AlignApp(App):
def compose(self):
yield Static("Vertical alignment with [b]Textual[/]", classes="box")
yield Static("Take note, browsers.", classes="box")
yield Label("Vertical alignment with [b]Textual[/]", classes="box")
yield Label("Take note, browsers.", classes="box")
app = AlignApp(css_path="align.css")

View File

@@ -0,0 +1,53 @@
#left-top {
/* align: left top; this is the default value and is implied. */
}
#center-top {
align: center top;
}
#right-top {
align: right top;
}
#left-middle {
align: left middle;
}
#center-middle {
align: center middle;
}
#right-middle {
align: right middle;
}
#left-bottom {
align: left bottom;
}
#center-bottom {
align: center bottom;
}
#right-bottom {
align: right bottom;
}
Screen {
layout: grid;
grid-size: 3 3;
grid-gutter: 1;
}
Container {
background: $boost;
border: solid gray;
height: 100%;
}
Label {
width: auto;
height: 1;
background: $accent;
}

View File

@@ -0,0 +1,20 @@
from textual.app import App, ComposeResult
from textual.containers import Container
from textual.widgets import Label
class AlignAllApp(App):
"""App that illustrates all alignments."""
CSS_PATH = "align_all.css"
def compose(self) -> ComposeResult:
yield Container(Label("left top"), id="left-top")
yield Container(Label("center top"), id="center-top")
yield Container(Label("right top"), id="right-top")
yield Container(Label("left middle"), id="left-middle")
yield Container(Label("center middle"), id="center-middle")
yield Container(Label("right middle"), id="right-middle")
yield Container(Label("left bottom"), id="left-bottom")
yield Container(Label("center bottom"), id="center-bottom")
yield Container(Label("right bottom"), id="right-bottom")

View File

@@ -1,14 +1,18 @@
Static {
Label {
width: 100%;
height: 1fr;
content-align: center middle;
color: white;
}
}
#static1 {
background: red;
}
#static2 {
background: rgb(0, 255, 0);
}
#static3 {
background: hsl(240, 100%, 50%);
}

View File

@@ -1,12 +1,12 @@
from textual.app import App
from textual.widgets import Static
from textual.widgets import Label
class BackgroundApp(App):
def compose(self):
yield Static("Widget 1", id="static1")
yield Static("Widget 2", id="static2")
yield Static("Widget 3", id="static3")
yield Label("Widget 1", id="static1")
yield Label("Widget 2", id="static2")
yield Label("Widget 3", id="static3")
app = BackgroundApp(css_path="background.css")

View File

@@ -0,0 +1,49 @@
#t10 {
background: red 10%;
}
#t20 {
background: red 20%;
}
#t30 {
background: red 30%;
}
#t40 {
background: red 40%;
}
#t50 {
background: red 50%;
}
#t60 {
background: red 60%;
}
#t70 {
background: red 70%;
}
#t80 {
background: red 80%;
}
#t90 {
background: red 90%;
}
#t100 {
background: red 100%;
}
Screen {
layout: horizontal;
}
Static {
height: 100%;
width: 1fr;
content-align: center middle;
}

View File

@@ -0,0 +1,20 @@
from textual.app import App, ComposeResult
from textual.widgets import Static
class BackgroundTransparencyApp(App):
"""Simple app to exemplify different transparency settings."""
def compose(self) -> ComposeResult:
yield Static("10%", id="t10")
yield Static("20%", id="t20")
yield Static("30%", id="t30")
yield Static("40%", id="t40")
yield Static("50%", id="t50")
yield Static("60%", id="t60")
yield Static("70%", id="t70")
yield Static("80%", id="t80")
yield Static("90%", id="t90")
yield Static("100%", id="t100")
app = BackgroundTransparencyApp(css_path="background_transparency.css")

View File

@@ -1,25 +1,30 @@
#label1 {
background: red 20%;
color: red;
border: solid red;
}
#label2 {
background: green 20%;
color: green;
border: dashed green;
}
#label3 {
background: blue 20%;
color: blue;
border: tall blue;
}
Screen {
background: white;
}
Screen > Static {
Screen > Label {
width: 100%;
height: 5;
content-align: center middle;
color: white;
margin: 1;
box-sizing: border-box;
}
#static1 {
background: red 20%;
color: red;
border: solid red;
}
#static2 {
background: green 20%;
color: green;
border: dashed green;
}
#static3 {
background: blue 20%;
color: blue;
border: tall blue;
}

View File

@@ -1,12 +1,12 @@
from textual.app import App
from textual.widgets import Static
from textual.widgets import Label
class BorderApp(App):
def compose(self):
yield Static("My border is solid red", id="static1")
yield Static("My border is dashed green", id="static2")
yield Static("My border is tall blue", id="static3")
yield Label("My border is solid red", id="label1")
yield Label("My border is dashed green", id="label2")
yield Label("My border is tall blue", id="label3")
app = BorderApp(css_path="border.css")

View File

@@ -0,0 +1,71 @@
#ascii {
border: ascii $accent;
}
#blank {
border: blank $accent;
}
#dashed {
border: dashed $accent;
}
#double {
border: double $accent;
}
#heavy {
border: heavy $accent;
}
#hidden {
border: hidden $accent;
}
#hkey {
border: hkey $accent;
}
#inner {
border: inner $accent;
}
#none {
border: none $accent;
}
#outer {
border: outer $accent;
}
#round {
border: round $accent;
}
#solid {
border: solid $accent;
}
#tall {
border: tall $accent;
}
#vkey {
border: vkey $accent;
}
#wide {
border: wide $accent;
}
Grid {
grid-size: 3 5;
align: center middle;
grid-gutter: 1 2;
}
Label {
width: 20;
height: 3;
content-align: center middle;
}

View File

@@ -0,0 +1,26 @@
from textual.app import App
from textual.containers import Grid
from textual.widgets import Label
class AllBordersApp(App):
def compose(self):
yield Grid(
Label("ascii", id="ascii"),
Label("blank", id="blank"),
Label("dashed", id="dashed"),
Label("double", id="double"),
Label("heavy", id="heavy"),
Label("hidden/none", id="hidden"),
Label("hkey", id="hkey"),
Label("inner", id="inner"),
Label("none", id="none"),
Label("outer", id="outer"),
Label("round", id="round"),
Label("solid", id="solid"),
Label("tall", id="tall"),
Label("vkey", id="vkey"),
Label("wide", id="wide"),
)
app = AllBordersApp(css_path="border_all.css")

View File

@@ -1,7 +1,16 @@
#static1 {
box-sizing: border-box;
}
#static2 {
box-sizing: content-box;
}
Screen {
background: white;
color: black;
}
App Static {
background: blue 20%;
height: 5;
@@ -9,9 +18,3 @@ App Static {
padding: 1;
border: wide black;
}
#static1 {
box-sizing: border-box;
}
#static2 {
box-sizing: content-box;
}

View File

@@ -1,13 +1,17 @@
Static {
height:1fr;
Label {
height: 1fr;
content-align: center middle;
}
#static1 {
width: 100%;
}
#label1 {
color: red;
}
#static2 {
#label2 {
color: rgb(0, 255, 0);
}
#static3 {
color: hsl(240, 100%, 50%)
#label3 {
color: hsl(240, 100%, 50%);
}

View File

@@ -1,12 +1,12 @@
from textual.app import App
from textual.widgets import Static
from textual.widgets import Label
class ColorApp(App):
def compose(self):
yield Static("I'm red!", id="static1")
yield Static("I'm rgb(0, 255, 0)!", id="static2")
yield Static("I'm hsl(240, 100%, 50%)!", id="static3")
yield Label("I'm red!", id="label1")
yield Label("I'm rgb(0, 255, 0)!", id="label2")
yield Label("I'm hsl(240, 100%, 50%)!", id="label3")
app = ColorApp(css_path="color.css")

View File

@@ -0,0 +1,26 @@
Label {
color: auto 80%;
content-align: center middle;
height: 1fr;
width: 100%;
}
#lbl1 {
background: red 80%;
}
#lbl2 {
background: yellow 80%;
}
#lbl3 {
background: blue 80%;
}
#lbl4 {
background: pink 80%;
}
#lbl5 {
background: green 80%;
}

View File

@@ -0,0 +1,14 @@
from textual.app import App
from textual.widgets import Label
class ColorApp(App):
def compose(self):
yield Label("The quick brown fox jumps over the lazy dog!", id="lbl1")
yield Label("The quick brown fox jumps over the lazy dog!", id="lbl2")
yield Label("The quick brown fox jumps over the lazy dog!", id="lbl3")
yield Label("The quick brown fox jumps over the lazy dog!", id="lbl4")
yield Label("The quick brown fox jumps over the lazy dog!", id="lbl5")
app = ColorApp(css_path="color_auto.css")

View File

@@ -0,0 +1,30 @@
#p1 {
column-span: 4;
}
#p2 {
column-span: 3;
}
#p3 {
column-span: 1; /* Didn't need to be set explicitly. */
}
#p4 {
column-span: 2;
}
#p5 {
column-span: 2;
}
#p6 {
/* Default value is 1. */
}
#p7 {
column-span: 3;
}
Grid {
grid-size: 4 4;
grid-gutter: 1 2;
}
Placeholder {
height: 100%;
}

View File

@@ -0,0 +1,19 @@
from textual.app import App
from textual.containers import Grid
from textual.widgets import Placeholder
class MyApp(App):
def compose(self):
yield Grid(
Placeholder(id="p1"),
Placeholder(id="p2"),
Placeholder(id="p3"),
Placeholder(id="p4"),
Placeholder(id="p5"),
Placeholder(id="p6"),
Placeholder(id="p7"),
)
app = MyApp(css_path="column_span.css")

View File

@@ -4,7 +4,8 @@
}
#box2 {
content-align: center middle;
content-align-horizontal: center;
content-align-vertical: middle;
background: green;
}
@@ -13,7 +14,8 @@
background: blue;
}
Static {
Label {
width: 100%;
height: 1fr;
padding: 1;
color: white;

View File

@@ -1,12 +1,12 @@
from textual.app import App
from textual.widgets import Static
from textual.widgets import Label
class ContentAlignApp(App):
def compose(self):
yield Static("With [i]content-align[/] you can...", id="box1")
yield Static("...[b]Easily align content[/]...", id="box2")
yield Static("...Horizontally [i]and[/] vertically!", id="box3")
yield Label("With [i]content-align[/] you can...", id="box1")
yield Label("...[b]Easily align content[/]...", id="box2")
yield Label("...Horizontally [i]and[/] vertically!", id="box3")
app = ContentAlignApp(css_path="content_align.css")

View File

@@ -0,0 +1,39 @@
#left-top {
/* content-align: left top; this is the default implied value. */
}
#center-top {
content-align: center top;
}
#right-top {
content-align: right top;
}
#left-middle {
content-align: left middle;
}
#center-middle {
content-align: center middle;
}
#right-middle {
content-align: right middle;
}
#left-bottom {
content-align: left bottom;
}
#center-bottom {
content-align: center bottom;
}
#right-bottom {
content-align: right bottom;
}
Screen {
layout: grid;
grid-size: 3 3;
grid-gutter: 1;
}
Label {
width: 100%;
height: 100%;
background: $primary;
}

View File

@@ -0,0 +1,18 @@
from textual.app import App
from textual.widgets import Label
class AllContentAlignApp(App):
def compose(self):
yield Label("left top", id="left-top")
yield Label("center top", id="center-top")
yield Label("right top", id="right-top")
yield Label("left middle", id="left-middle")
yield Label("center middle", id="center-middle")
yield Label("right middle", id="right-middle")
yield Label("left bottom", id="left-bottom")
yield Label("center bottom", id="center-bottom")
yield Label("right bottom", id="right-bottom")
app = AllContentAlignApp(css_path="content_align_all.css")

View File

@@ -1,12 +1,14 @@
Screen {
background: green;
}
Static {
height: 5;
background: white;
color: blue;
border: heavy blue;
Static {
height: 5;
background: white;
color: blue;
border: heavy blue;
}
Static.remove {
display: none;
}

View File

@@ -0,0 +1,34 @@
#left {
dock: left;
height: 100%;
width: auto;
align-vertical: middle;
}
#top {
dock: top;
height: auto;
width: 100%;
align-horizontal: center;
}
#right {
dock: right;
height: 100%;
width: auto;
align-vertical: middle;
}
#bottom {
dock: bottom;
height: auto;
width: 100%;
align-horizontal: center;
}
Screen {
align: center middle;
}
#big_container {
width: 75%;
height: 75%;
border: round white;
}

View File

@@ -0,0 +1,17 @@
from textual.app import App
from textual.containers import Container
from textual.widgets import Label
class DockAllApp(App):
def compose(self):
yield Container(
Container(Label("left"), id="left"),
Container(Label("top"), id="top"),
Container(Label("right"), id="right"),
Container(Label("bottom"), id="bottom"),
id="big_container",
)
app = DockAllApp(css_path="dock_all.css")

View File

@@ -0,0 +1,11 @@
Grid {
grid-size: 5 2;
grid-columns: 1fr 16 2fr;
}
Label {
border: round white;
content-align-horizontal: center;
width: 100%;
height: 100%;
}

View File

@@ -0,0 +1,22 @@
from textual.app import App
from textual.containers import Grid
from textual.widgets import Label
class MyApp(App):
def compose(self):
yield Grid(
Label("1fr"),
Label("width = 16"),
Label("2fr"),
Label("1fr"),
Label("width = 16"),
Label("1fr"),
Label("width = 16"),
Label("2fr"),
Label("1fr"),
Label("width = 16"),
)
app = MyApp(css_path="grid_columns.css")

View File

@@ -0,0 +1,11 @@
Grid {
grid-size: 2 4;
grid-gutter: 1 2; /* (1)! */
}
Label {
border: round white;
content-align: center middle;
width: 100%;
height: 100%;
}

View File

@@ -0,0 +1,20 @@
from textual.app import App
from textual.containers import Grid
from textual.widgets import Label
class MyApp(App):
def compose(self):
yield Grid(
Label("1"),
Label("2"),
Label("3"),
Label("4"),
Label("5"),
Label("6"),
Label("7"),
Label("8"),
)
app = MyApp(css_path="grid_gutter.css")

View File

@@ -0,0 +1,11 @@
Grid {
grid-size: 2 5;
grid-rows: 1fr 6 25%;
}
Label {
border: round white;
content-align: center middle;
width: 100%;
height: 100%;
}

View File

@@ -0,0 +1,22 @@
from textual.app import App
from textual.containers import Grid
from textual.widgets import Label
class MyApp(App):
def compose(self):
yield Grid(
Label("1fr"),
Label("1fr"),
Label("height = 6"),
Label("height = 6"),
Label("25%"),
Label("25%"),
Label("1fr"),
Label("1fr"),
Label("height = 6"),
Label("height = 6"),
)
app = MyApp(css_path="grid_rows.css")

View File

@@ -0,0 +1,10 @@
Grid {
grid-size: 2 4; /* (1)! */
}
Label {
border: round white;
content-align: center middle;
width: 100%;
height: 100%;
}

View File

@@ -0,0 +1,17 @@
from textual.app import App
from textual.containers import Grid
from textual.widgets import Label
class MyApp(App):
def compose(self):
yield Grid(
Label("1"),
Label("2"),
Label("3"),
Label("4"),
Label("5"),
)
app = MyApp(css_path="grid_size_both.css")

View File

@@ -0,0 +1,10 @@
Grid {
grid-size: 2; /* (1)! */
}
Label {
border: round white;
content-align: center middle;
width: 100%;
height: 100%;
}

View File

@@ -0,0 +1,17 @@
from textual.app import App
from textual.containers import Grid
from textual.widgets import Label
class MyApp(App):
def compose(self):
yield Grid(
Label("1"),
Label("2"),
Label("3"),
Label("4"),
Label("5"),
)
app = MyApp(css_path="grid_size_columns.css")

View File

@@ -1,4 +1,4 @@
Screen > Widget {
Screen > Widget {
background: green;
height: 50%;
color: white;

View File

@@ -0,0 +1,39 @@
#cells {
height: 2; /* (1)! */
}
#percent {
height: 12.5%; /* (2)! */
}
#w {
height: 5w; /* (3)! */
}
#h {
height: 12.5h; /* (4)! */
}
#vw {
height: 6.25vw; /* (5)! */
}
#vh {
height: 12.5vh; /* (6)! */
}
#auto {
height: auto; /* (7)! */
}
#fr1 {
height: 1fr; /* (8)! */
}
#fr2 {
height: 2fr; /* (9)! */
}
Screen {
layers: ruler;
}
Ruler {
layer: ruler;
dock: right;
overflow: hidden;
width: 1;
background: $accent;
}

View File

@@ -0,0 +1,28 @@
from textual.app import App
from textual.containers import Vertical
from textual.widgets import Placeholder, Label, Static
class Ruler(Static):
def compose(self):
ruler_text = "·\n·\n·\n·\n\n" * 100
yield Label(ruler_text)
class HeightComparisonApp(App):
def compose(self):
yield Vertical(
Placeholder(id="cells"), # (1)!
Placeholder(id="percent"),
Placeholder(id="w"),
Placeholder(id="h"),
Placeholder(id="vw"),
Placeholder(id="vh"),
Placeholder(id="auto"),
Placeholder(id="fr1"),
Placeholder(id="fr2"),
)
yield Ruler()
app = HeightComparisonApp(css_path="height_comparison.css")

View File

@@ -10,7 +10,7 @@
height: auto;
}
Static {
Label {
margin: 1;
width: 12;
color: black;

View File

@@ -1,20 +1,20 @@
from textual.app import App
from textual.containers import Container
from textual.widgets import Static
from textual.widgets import Label
class LayoutApp(App):
def compose(self):
yield Container(
Static("Layout"),
Static("Is"),
Static("Vertical"),
Label("Layout"),
Label("Is"),
Label("Vertical"),
id="vertical-layout",
)
yield Container(
Static("Layout"),
Static("Is"),
Static("Horizontal"),
Label("Layout"),
Label("Is"),
Label("Horizontal"),
id="horizontal-layout",
)

View File

@@ -0,0 +1,11 @@
#lbl1, #lbl2 {
link-background: red; /* (1)! */
}
#lbl3 {
link-background: hsl(60,100%,50%) 50%;
}
#lbl4 {
link-background: $accent;
}

View File

@@ -0,0 +1,25 @@
from textual.app import App
from textual.widgets import Label
class LinkBackgroundApp(App):
def compose(self):
yield Label(
"Visit the [link=https://textualize.io]Textualize[/link] website.",
id="lbl1", # (1)!
)
yield Label(
"Click [@click=app.bell]here[/] for the bell sound.",
id="lbl2", # (2)!
)
yield Label(
"You can also click [@click=app.bell]here[/] for the bell sound.",
id="lbl3", # (3)!
)
yield Label(
"[@click=app.quit]Exit this application.[/]",
id="lbl4", # (4)!
)
app = LinkBackgroundApp(css_path="link_background.css")

View File

@@ -0,0 +1,11 @@
#lbl1, #lbl2 {
link-color: red; /* (1)! */
}
#lbl3 {
link-color: hsl(60,100%,50%) 50%;
}
#lbl4 {
link-color: $accent;
}

View File

@@ -0,0 +1,25 @@
from textual.app import App
from textual.widgets import Label
class LinkColorApp(App):
def compose(self):
yield Label(
"Visit the [link=https://textualize.io]Textualize[/link] website.",
id="lbl1", # (1)!
)
yield Label(
"Click [@click=app.bell]here[/] for the bell sound.",
id="lbl2", # (2)!
)
yield Label(
"You can also click [@click=app.bell]here[/] for the bell sound.",
id="lbl3", # (3)!
)
yield Label(
"[@click=app.quit]Exit this application.[/]",
id="lbl4", # (4)!
)
app = LinkColorApp(css_path="link_color.css")

View File

@@ -0,0 +1,11 @@
#lbl1, #lbl2 {
link-hover-background: red; /* (1)! */
}
#lbl3 {
link-hover-background: hsl(60,100%,50%) 50%;
}
#lbl4 {
/* Empty to show the default hover background */ /* (2)! */
}

View File

@@ -0,0 +1,25 @@
from textual.app import App
from textual.widgets import Label
class LinkHoverBackgroundApp(App):
def compose(self):
yield Label(
"Visit the [link=https://textualize.io]Textualize[/link] website.",
id="lbl1", # (1)!
)
yield Label(
"Click [@click=app.bell]here[/] for the bell sound.",
id="lbl2", # (2)!
)
yield Label(
"You can also click [@click=app.bell]here[/] for the bell sound.",
id="lbl3", # (3)!
)
yield Label(
"[@click=app.quit]Exit this application.[/]",
id="lbl4", # (4)!
)
app = LinkHoverBackgroundApp(css_path="link_hover_background.css")

View File

@@ -0,0 +1,11 @@
#lbl1, #lbl2 {
link-hover-color: red; /* (1)! */
}
#lbl3 {
link-hover-color: hsl(60,100%,50%) 50%;
}
#lbl4 {
link-hover-color: black;
}

View File

@@ -0,0 +1,25 @@
from textual.app import App
from textual.widgets import Label
class LinkHoverColorApp(App):
def compose(self):
yield Label(
"Visit the [link=https://textualize.io]Textualize[/link] website.",
id="lbl1", # (1)!
)
yield Label(
"Click [@click=app.bell]here[/] for the bell sound.",
id="lbl2", # (2)!
)
yield Label(
"You can also click [@click=app.bell]here[/] for the bell sound.",
id="lbl3", # (3)!
)
yield Label(
"[@click=app.quit]Exit this application.[/]",
id="lbl4", # (4)!
)
app = LinkHoverColorApp(css_path="link_hover_color.css")

View File

@@ -0,0 +1,11 @@
#lbl1, #lbl2 {
link-hover-style: bold italic; /* (1)! */
}
#lbl3 {
link-hover-style: reverse strike;
}
#lbl4 {
link-hover-style: bold;
}

View File

@@ -0,0 +1,25 @@
from textual.app import App
from textual.widgets import Label
class LinkHoverStyleApp(App):
def compose(self):
yield Label(
"Visit the [link=https://textualize.io]Textualize[/link] website.",
id="lbl1", # (1)!
)
yield Label(
"Click [@click=app.bell]here[/] for the bell sound.",
id="lbl2", # (2)!
)
yield Label(
"You can also click [@click=app.bell]here[/] for the bell sound.",
id="lbl3", # (3)!
)
yield Label(
"[@click=app.quit]Exit this application.[/]",
id="lbl4", # (4)!
)
app = LinkHoverStyleApp(css_path="link_hover_style.css")

View File

@@ -0,0 +1,11 @@
#lbl1, #lbl2 {
link-style: bold italic; /* (1)! */
}
#lbl3 {
link-style: reverse strike;
}
#lbl4 {
link-style: bold;
}

View File

@@ -0,0 +1,25 @@
from textual.app import App
from textual.widgets import Label
class LinkStyleApp(App):
def compose(self):
yield Label(
"Visit the [link=https://textualize.io]Textualize[/link] website.",
id="lbl1", # (1)!
)
yield Label(
"Click [@click=app.bell]here[/] for the bell sound.",
id="lbl2", # (2)!
)
yield Label(
"You can also click [@click=app.bell]here[/] for the bell sound.",
id="lbl3", # (3)!
)
yield Label(
"[@click=app.quit]Exit this application.[/]",
id="lbl4", # (4)!
)
app = LinkStyleApp(css_path="link_style.css")

View File

@@ -3,8 +3,9 @@ Screen {
color: black;
}
Static {
margin: 4 8;
background: blue 20%;
Label {
margin: 4 8;
background: blue 20%;
border: blue wide;
}
width: 100%;
}

View File

@@ -1,5 +1,5 @@
from textual.app import App
from textual.widgets import Static
from textual.widgets import Label
TEXT = """I must not fear.
Fear is the mind-killer.
@@ -12,7 +12,7 @@ Where the fear has gone there will be nothing. Only I will remain."""
class MarginApp(App):
def compose(self):
yield Static(TEXT)
yield Label(TEXT)
app = MarginApp(css_path="margin.css")

View File

@@ -0,0 +1,54 @@
Screen {
background: $background;
}
Grid {
grid-size: 4;
grid-gutter: 1 2;
}
Placeholder {
width: 100%;
height: 100%;
}
Container {
width: 100%;
height: 100%;
}
.bordered {
border: white round;
}
#p1 {
/* default is no margin */
}
#p2 {
margin: 1;
}
#p3 {
margin: 1 5;
}
#p4 {
margin: 1 1 2 6;
}
#p5 {
margin-top: 4;
}
#p6 {
margin-right: 3;
}
#p7 {
margin-bottom: 4;
}
#p8 {
margin-left: 3;
}

View File

@@ -0,0 +1,20 @@
from textual.app import App
from textual.containers import Container, Grid
from textual.widgets import Placeholder
class MarginAllApp(App):
def compose(self):
yield Grid(
Container(Placeholder("no margin", id="p1"), classes="bordered"),
Container(Placeholder("margin: 1", id="p2"), classes="bordered"),
Container(Placeholder("margin: 1 5", id="p3"), classes="bordered"),
Container(Placeholder("margin: 1 1 2 6", id="p4"), classes="bordered"),
Container(Placeholder("margin-top: 4", id="p5"), classes="bordered"),
Container(Placeholder("margin-right: 3", id="p6"), classes="bordered"),
Container(Placeholder("margin-bottom: 4", id="p7"), classes="bordered"),
Container(Placeholder("margin-left: 3", id="p8"), classes="bordered"),
)
app = MarginAllApp(css_path="margin_all.css")

View File

@@ -0,0 +1,25 @@
Horizontal {
height: 100%;
width: 100%;
}
Placeholder {
height: 100%;
width: 1fr;
}
#p1 {
max-height: 10w;
}
#p2 {
max-height: 999; /* (1)! */
}
#p3 {
max-height: 50%;
}
#p4 {
max-height: 10;
}

View File

@@ -0,0 +1,16 @@
from textual.app import App
from textual.containers import Horizontal
from textual.widgets import Placeholder
class MaxHeightApp(App):
def compose(self):
yield Horizontal(
Placeholder("max-height: 10w", id="p1"),
Placeholder("max-height: 999", id="p2"),
Placeholder("max-height: 50%", id="p3"),
Placeholder("max-height: 10", id="p4"),
)
app = MaxHeightApp(css_path="max_height.css")

View File

@@ -0,0 +1,25 @@
Horizontal {
height: 100%;
width: 100%;
}
Placeholder {
width: 100%;
height: 1fr;
}
#p1 {
max-width: 50h;
}
#p2 {
max-width: 999; /* (1)! */
}
#p3 {
max-width: 50%;
}
#p4 {
max-width: 30;
}

View File

@@ -0,0 +1,16 @@
from textual.app import App
from textual.containers import Vertical
from textual.widgets import Placeholder
class MaxWidthApp(App):
def compose(self):
yield Vertical(
Placeholder("max-width: 50h", id="p1"),
Placeholder("max-width: 999", id="p2"),
Placeholder("max-width: 50%", id="p3"),
Placeholder("max-width: 30", id="p4"),
)
app = MaxWidthApp(css_path="max_width.css")

View File

@@ -0,0 +1,26 @@
Horizontal {
height: 100%;
width: 100%;
overflow-y: auto;
}
Placeholder {
width: 1fr;
height: 50%;
}
#p1 {
min-height: 25%; /* (1)! */
}
#p2 {
min-height: 75%;
}
#p3 {
min-height: 30;
}
#p4 {
min-height: 40w;
}

View File

@@ -0,0 +1,16 @@
from textual.app import App
from textual.containers import Horizontal
from textual.widgets import Placeholder
class MinHeightApp(App):
def compose(self):
yield Horizontal(
Placeholder("min-height: 25%", id="p1"),
Placeholder("min-height: 75%", id="p2"),
Placeholder("min-height: 30", id="p3"),
Placeholder("min-height: 40w", id="p4"),
)
app = MinHeightApp(css_path="min_height.css")

View File

@@ -0,0 +1,26 @@
Vertical {
height: 100%;
width: 100%;
overflow-x: auto;
}
Placeholder {
height: 1fr;
width: 50%;
}
#p1 {
min-width: 25%; /* (1)! */
}
#p2 {
min-width: 75%;
}
#p3 {
min-width: 100;
}
#p4 {
min-width: 400h;
}

View File

@@ -0,0 +1,16 @@
from textual.app import App
from textual.containers import Vertical
from textual.widgets import Placeholder
class MinWidthApp(App):
def compose(self):
yield Vertical(
Placeholder("min-width: 25%", id="p1"),
Placeholder("min-width: 75%", id="p2"),
Placeholder("min-width: 100", id="p3"),
Placeholder("min-width: 400h", id="p4"),
)
app = MinWidthApp(css_path="min_width.css")

View File

@@ -3,10 +3,10 @@ Screen {
color: black;
layout: horizontal;
}
Static {
Label {
width: 20;
height: 10;
content-align: center middle;
content-align: center middle;
}
.paul {
@@ -24,7 +24,7 @@ Static {
}
.chani {
offset: 0 5;
offset: 0 -3;
background: blue 20%;
border: outer blue;
color: blue;

View File

@@ -1,12 +1,12 @@
from textual.app import App
from textual.widgets import Static
from textual.widgets import Label
class OffsetApp(App):
def compose(self):
yield Static("Paul (offset 8 2)", classes="paul")
yield Static("Duncan (offset 4 10)", classes="duncan")
yield Static("Chani (offset 0 5)", classes="chani")
yield Label("Paul (offset 8 2)", classes="paul")
yield Label("Duncan (offset 4 10)", classes="duncan")
yield Label("Chani (offset 0 -3)", classes="chani")
app = OffsetApp(css_path="offset.css")

View File

@@ -19,10 +19,11 @@
}
Screen {
background: antiquewhite;
background: black;
}
Static {
Label {
width: 100%;
height: 1fr;
border: outer dodgerblue;
background: lightseagreen;

View File

@@ -1,14 +1,14 @@
from textual.app import App
from textual.widgets import Static
from textual.widgets import Label
class OpacityApp(App):
def compose(self):
yield Static("opacity: 0%", id="zero-opacity")
yield Static("opacity: 25%", id="quarter-opacity")
yield Static("opacity: 50%", id="half-opacity")
yield Static("opacity: 75%", id="three-quarter-opacity")
yield Static("opacity: 100%", id="full-opacity")
yield Label("opacity: 0%", id="zero-opacity")
yield Label("opacity: 25%", id="quarter-opacity")
yield Label("opacity: 50%", id="half-opacity")
yield Label("opacity: 75%", id="three-quarter-opacity")
yield Label("opacity: 100%", id="full-opacity")
app = OpacityApp(css_path="opacity.css")

View File

@@ -2,8 +2,10 @@ Screen {
background: white;
color: black;
}
Static {
Label {
margin: 4 8;
background: green 20%;
outline: wide green;
width: 100%;
}

View File

@@ -1,5 +1,5 @@
from textual.app import App
from textual.widgets import Static
from textual.widgets import Label
TEXT = """I must not fear.
@@ -13,7 +13,7 @@ Where the fear has gone there will be nothing. Only I will remain."""
class OutlineApp(App):
def compose(self):
yield Static(TEXT)
yield Label(TEXT)
app = OutlineApp(css_path="outline.css")

View File

@@ -0,0 +1,71 @@
#ascii {
outline: ascii $accent;
}
#blank {
outline: blank $accent;
}
#dashed {
outline: dashed $accent;
}
#double {
outline: double $accent;
}
#heavy {
outline: heavy $accent;
}
#hidden {
outline: hidden $accent;
}
#hkey {
outline: hkey $accent;
}
#inner {
outline: inner $accent;
}
#none {
outline: none $accent;
}
#outer {
outline: outer $accent;
}
#round {
outline: round $accent;
}
#solid {
outline: solid $accent;
}
#tall {
outline: tall $accent;
}
#vkey {
outline: vkey $accent;
}
#wide {
outline: wide $accent;
}
Grid {
grid-size: 3 5;
align: center middle;
grid-gutter: 1 2;
}
Label {
width: 20;
height: 3;
content-align: center middle;
}

View File

@@ -0,0 +1,26 @@
from textual.app import App
from textual.containers import Grid
from textual.widgets import Label
class AllOutlinesApp(App):
def compose(self):
yield Grid(
Label("ascii", id="ascii"),
Label("blank", id="blank"),
Label("dashed", id="dashed"),
Label("double", id="double"),
Label("heavy", id="heavy"),
Label("hidden/none", id="hidden"),
Label("hkey", id="hkey"),
Label("inner", id="inner"),
Label("none", id="none"),
Label("outer", id="outer"),
Label("round", id="round"),
Label("solid", id="solid"),
Label("tall", id="tall"),
Label("vkey", id="vkey"),
Label("wide", id="wide"),
)
app = AllOutlinesApp(css_path="outline_all.css")

View File

@@ -0,0 +1,11 @@
Label {
height: 8;
}
.outline {
outline: $error round;
}
.border {
border: $success heavy;
}

View File

@@ -0,0 +1,21 @@
from textual.app import App
from textual.widgets import Label
TEXT = """I must not fear.
Fear is the mind-killer.
Fear is the little-death that brings total obliteration.
I will face my fear.
I will permit it to pass over me and through me.
And when it has gone past, I will turn the inner eye to see its path.
Where the fear has gone there will be nothing. Only I will remain."""
class OutlineBorderApp(App):
def compose(self):
yield Label(TEXT, classes="outline")
yield Label(TEXT, classes="border")
yield Label(TEXT, classes="outline border")
app = OutlineBorderApp(css_path="outline_vs_border.css")

View File

@@ -8,8 +8,8 @@ Vertical {
}
Static {
margin: 1 2;
background: green 80%;
margin: 1 2;
background: green 80%;
border: green wide;
color: white 90%;
height: auto;

View File

@@ -3,7 +3,8 @@ Screen {
color: blue;
}
Static {
padding: 4 8;
background: blue 20%;
}
Label {
padding: 4 8;
background: blue 20%;
width: 100%;
}

View File

@@ -1,5 +1,5 @@
from textual.app import App
from textual.widgets import Static
from textual.widgets import Label
TEXT = """I must not fear.
Fear is the mind-killer.
@@ -12,7 +12,7 @@ Where the fear has gone there will be nothing. Only I will remain."""
class PaddingApp(App):
def compose(self):
yield Static(TEXT)
yield Label(TEXT)
app = PaddingApp(css_path="padding.css")

View File

@@ -0,0 +1,45 @@
Screen {
background: $background;
}
Grid {
grid-size: 4;
grid-gutter: 1 2;
}
Placeholder {
width: auto;
height: auto;
}
#p1 {
/* default is no padding */
}
#p2 {
padding: 1;
}
#p3 {
padding: 1 5;
}
#p4 {
padding: 1 1 2 6;
}
#p5 {
padding-top: 4;
}
#p6 {
padding-right: 3;
}
#p7 {
padding-bottom: 4;
}
#p8 {
padding-left: 3;
}

View File

@@ -0,0 +1,20 @@
from textual.app import App
from textual.containers import Container, Grid
from textual.widgets import Placeholder
class PaddingAllApp(App):
def compose(self):
yield Grid(
Placeholder("no padding", id="p1"),
Placeholder("padding: 1", id="p2"),
Placeholder("padding: 1 5", id="p3"),
Placeholder("padding: 1 1 2 6", id="p4"),
Placeholder("padding-top: 4", id="p5"),
Placeholder("padding-right: 3", id="p6"),
Placeholder("padding-bottom: 4", id="p7"),
Placeholder("padding-left: 3", id="p8"),
)
app = PaddingAllApp(css_path="padding_all.css")

View File

@@ -0,0 +1,30 @@
#p1 {
row-span: 4;
}
#p2 {
row-span: 3;
}
#p3 {
row-span: 2;
}
#p4 {
row-span: 1; /* Didn't need to be set explicitly. */
}
#p5 {
row-span: 3;
}
#p6 {
row-span: 2;
}
#p7 {
/* Default value is 1. */
}
Grid {
grid-size: 4 4;
grid-gutter: 1 2;
}
Placeholder {
height: 100%;
}

Some files were not shown because too many files have changed in this diff Show More