mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
docs
This commit is contained in:
@@ -16,7 +16,7 @@ CSS is typically stored in an external file with the extension `.css` alongside
|
||||
|
||||
Let's look at some Textual CSS.
|
||||
|
||||
```css
|
||||
```sass
|
||||
Header {
|
||||
dock: top;
|
||||
height: 3;
|
||||
@@ -28,7 +28,7 @@ Header {
|
||||
|
||||
This is an example of a CSS _rule set_. There may be many such sections in any given CSS file.
|
||||
|
||||
The first line is a _selector_, which tells Textual which Widget(s) to modify. In the above example, the styles will be applied to a widget defined in the Python class `Header`.
|
||||
Let's break this CSS code down a bit.
|
||||
|
||||
```css hl_lines="1"
|
||||
Header {
|
||||
@@ -40,7 +40,7 @@ Header {
|
||||
}
|
||||
```
|
||||
|
||||
The lines inside the curly braces contains CSS _rules_, which consist of a rule name and rule value separated by a colon and ending in a semi-colon. Such rules are typically written one per line, but you could add additional rules as long as they are separated by semi-colons.
|
||||
The first line is a _selector_ which tells Textual which Widget(s) to modify. In the above example, the styles will be applied to a widget defined in the Python class `Header`.
|
||||
|
||||
```css hl_lines="2 3 4 5 6"
|
||||
Header {
|
||||
@@ -52,6 +52,8 @@ Header {
|
||||
}
|
||||
```
|
||||
|
||||
The lines inside the curly braces contains CSS _rules_, which consist of a rule name and rule value separated by a colon and ending in a semi-colon. Such rules are typically written one per line, but you could add additional rules as long as they are separated by semi-colons.
|
||||
|
||||
The first rule in the above example reads `"dock: top;"`. The rule name is `dock` which tells Textual to place the widget on a edge of the screen. The text after the colon is `top` which tells Textual to dock to the _top_ of the screen. Other valid values for dock are "right", "bottom", or "left"; but `top` is naturally appropriate for a header.
|
||||
|
||||
You may be able to guess what some of the the other rules do. We will cover those later.
|
||||
@@ -75,7 +77,7 @@ Let's look at a trivial Textual app.
|
||||
```{.textual path="docs/examples/guide/dom1.py"}
|
||||
```
|
||||
|
||||
When you run this code you will have an instance of an app (ExampleApp) in memory. This app class will also create a Screen object. In DOM terms, the Screen is a _child_ of the app.
|
||||
When you run this code you will have an instance of an `ExampleApp` in memory. This app class will also create a `Screen` object. In DOM terms, the Screen is a _child_ of the app.
|
||||
|
||||
With the above example, the DOM will look like the following:
|
||||
|
||||
@@ -121,7 +123,7 @@ To further explore the DOM, we're going to build a simple dialog with a question
|
||||
--8<-- "docs/examples/guide/dom3.py"
|
||||
```
|
||||
|
||||
We've added a Container to our DOM which (as the name suggests) is a container for other widgets. The container has a number of other widgets passed as positional arguments which will be added as the children of the container. Not all widgets accept child widgets in this way; for instance a Button widget doesn't need any children.
|
||||
We've added a Container to our DOM which (as the name suggests) is a container for other widgets. The container has a number of other widgets passed as positional arguments which will be added as the children of the container. Not all widgets accept child widgets in this way. A Button widget doesn't require any children, for example.
|
||||
|
||||
Here's the DOM created by the above code:
|
||||
|
||||
@@ -149,7 +151,7 @@ You may have noticed that some of the constructors have additional keywords argu
|
||||
|
||||
Here's the CSS file we are applying:
|
||||
|
||||
```python
|
||||
```sass
|
||||
--8<-- "docs/examples/guide/dom4.css"
|
||||
```
|
||||
|
||||
@@ -175,7 +177,7 @@ Finally, Textual CSS allows you to _live edit_ the styles in your app. If you ru
|
||||
textual run my_app.py --dev
|
||||
```
|
||||
|
||||
Being able to iterate on the design without restarting the Python code can make it much easier to design beautiful interfaces.
|
||||
Being able to iterate on the design without restarting the Python code can make it easier and faster to design beautiful interfaces.
|
||||
|
||||
## Selectors
|
||||
|
||||
@@ -198,7 +200,7 @@ class Button(Static):
|
||||
|
||||
The following rule applies a border to this widget:
|
||||
|
||||
```css
|
||||
```sass
|
||||
Button {
|
||||
border: solid blue;
|
||||
}
|
||||
@@ -206,7 +208,7 @@ Button {
|
||||
|
||||
The type selector will also match a widget's base classes. Consequently a `Static` selector will also style the button because the `Button` Python class extends `Static`.
|
||||
|
||||
```css
|
||||
```sass
|
||||
Static {
|
||||
background: blue;
|
||||
border: rounded white;
|
||||
@@ -231,15 +233,17 @@ yield Button(id="next")
|
||||
|
||||
You can match an ID with a selector starting with a hash (`#`). Here is how you might draw a red outline around the above button:
|
||||
|
||||
```css
|
||||
```sass
|
||||
#next {
|
||||
outline: red;
|
||||
}
|
||||
```
|
||||
|
||||
A Widget's `id` attribute can not be changed after the Widget has been constructed.
|
||||
|
||||
### Class-name selector
|
||||
|
||||
Every widget can have a number of class names applied. The term "class" here is borrowed from web CSS, and has a different meaning to a Python class. You can think of a CSS class as a tag of sorts. Widgets with the same tag may share a particular style.
|
||||
Every widget can have a number of class names applied. The term "class" here is borrowed from web CSS, and has a different meaning to a Python class. You can think of a CSS class as a tag of sorts. Widgets with the same tag will share styles.
|
||||
|
||||
CSS classes are set via the widgets `classes` parameter in the constructor. Here's an example:
|
||||
|
||||
@@ -257,7 +261,7 @@ yield Button(classes="error disabled")
|
||||
|
||||
To match a Widget with a given class in CSS you can precede the class name with a dot (`.`). Here's a rule with a class selector to match the `"success"` class name:
|
||||
|
||||
```css
|
||||
```sass
|
||||
.success {
|
||||
background: green;
|
||||
color: white;
|
||||
@@ -270,19 +274,28 @@ To match a Widget with a given class in CSS you can precede the class name with
|
||||
|
||||
Class name selectors may be _chained_ together by appending another full stop and class name. The selector will match a widget that has _all_ of the class names set. For instance, the following sets a red background on widgets that have both `error` _and_ `disabled` class names.
|
||||
|
||||
```css
|
||||
```sass
|
||||
.error.disabled {
|
||||
background: darkred;
|
||||
}
|
||||
```
|
||||
|
||||
Unlike the `id` attribute a Widget's classes can be changed after the Widget was created. Adding and removing CSS classes is the recommended way of changing the display while your app is running. There are a few methods you can use to manage CSS classes.
|
||||
|
||||
- [add_class()][textual.dom.DOMNode.add_class] Adds one or more classes to a widget.
|
||||
- [remove_class()][textual.dom.DOMNode.remove_class] Removes class name(s) from a widget.
|
||||
- [toggle_class()][textual.dom.DOMNode.toggle_class] Removes a class name if it is present, or adds the name if its not already present.
|
||||
- [has_class()][textual.dom.DOMNode.has_class] Checks if a class(es) is set on a widget.
|
||||
- [classes][textual.dom.DOMNode.classes] Is a frozen set of the class(es) set on a widget.
|
||||
|
||||
|
||||
### Universal selector
|
||||
|
||||
The _universal_ selectors is denoted by an asterisk and will match _all_ widgets.
|
||||
|
||||
For example, the following will draw a red outline around all widgets:
|
||||
|
||||
```css
|
||||
```sass
|
||||
* {
|
||||
outline: solid red;
|
||||
}
|
||||
@@ -292,7 +305,7 @@ For example, the following will draw a red outline around all widgets:
|
||||
|
||||
Pseudo classes can be used to match widgets in a particular state. Psuedo classes are set automatically by Textual. For instance, you might want a button to have a green background when the mouse cursor moves over it. We can do this with the `:hover` pseudo selector.
|
||||
|
||||
```css
|
||||
```sass
|
||||
Button:hover {
|
||||
background: green;
|
||||
}
|
||||
@@ -321,7 +334,7 @@ Here's a section of DOM to illustrate this combinator:
|
||||
|
||||
Let's say we want to make the text of the buttons in the dialog bold, but we _don't_ want to change the Button in the sidebar. We can do this with the following rule:
|
||||
|
||||
```css hl_lines="1"
|
||||
```sass hl_lines="1"
|
||||
#dialog Button {
|
||||
text-style: bold;
|
||||
}
|
||||
@@ -349,7 +362,7 @@ Let's use this to match the Button in the sidebar given the following DOM:
|
||||
|
||||
We can use the following CSS to style all buttons which have a parent with an ID of `sidebar`:
|
||||
|
||||
```css
|
||||
```sass
|
||||
#sidebar > Button {
|
||||
text-style: underline;
|
||||
}
|
||||
@@ -375,7 +388,7 @@ The specificity rules are usually enough to fix any conflicts in your stylesheet
|
||||
|
||||
Here's an example that makes buttons blue when hovered over with the mouse, regardless of any other selectors that match Buttons:
|
||||
|
||||
```css hl_lines="2"
|
||||
```sass hl_lines="2"
|
||||
Button:hover {
|
||||
background: blue !important;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ You can run Textual apps with the `run` subcommand. If you supply a path to a Py
|
||||
textual run my_app.py
|
||||
```
|
||||
|
||||
The `run` sub-command assumes you have a Application instance called `app` in the global scope of your Python file. If the application is called something different, you can specify it with a colon following the filename:
|
||||
The `run` sub-command assumes you have a App instance called `app` in the global scope of your Python file. If the application is called something different, you can specify it with a colon following the filename:
|
||||
|
||||
```
|
||||
textual run my_app.py:alternative_app
|
||||
@@ -24,7 +24,7 @@ textual run my_app.py:alternative_app
|
||||
|
||||
!!! note
|
||||
|
||||
If the Python file contains a call to app.run() then you can launch the file as you normally would any other Python program. Running your app via `textual run` will give you access to a few Textual features such as dev mode which auto (re) loads your CSS if you change it.
|
||||
If the Python file contains a call to app.run() then you can launch the file as you normally would any other Python program. Running your app via `textual run` will give you access to a few Textual features such as live editing of CSS files.
|
||||
|
||||
## Console
|
||||
|
||||
@@ -44,7 +44,7 @@ This should look something like the following:
|
||||
In the other console, run your application using `textual run` and the `--dev` switch:
|
||||
|
||||
```bash
|
||||
textual run my_app.py --dev
|
||||
textual run --dev my_app.py
|
||||
```
|
||||
|
||||
Anything you `print` from your application will be displayed in the console window. You can also call the `log()` method on App and Widget objects for advanced formatting. Try it with `self.log(self.tree)`.
|
||||
|
||||
1
docs/reference/color.md
Normal file
1
docs/reference/color.md
Normal file
@@ -0,0 +1 @@
|
||||
::: textual.color
|
||||
1
docs/reference/dom_node.md
Normal file
1
docs/reference/dom_node.md
Normal file
@@ -0,0 +1 @@
|
||||
::: textual.dom.DOMNode
|
||||
18
mkdocs.yml
18
mkdocs.yml
@@ -6,8 +6,8 @@ nav:
|
||||
- "getting_started.md"
|
||||
- "introduction.md"
|
||||
- Guide:
|
||||
- "guide/guide.md"
|
||||
- "guide/devtools.md"
|
||||
- "guide/guide.md"
|
||||
- "guide/CSS.md"
|
||||
- "guide/events.md"
|
||||
|
||||
@@ -44,19 +44,19 @@ nav:
|
||||
- "styles/color.md"
|
||||
- "styles/content_align.md"
|
||||
- "styles/display.md"
|
||||
- "styles/min_height.md"
|
||||
- "styles/max_height.md"
|
||||
- "styles/min_width.md"
|
||||
- "styles/max_width.md"
|
||||
- "styles/height.md"
|
||||
- "styles/margin.md"
|
||||
- "styles/max_height.md"
|
||||
- "styles/max_width.md"
|
||||
- "styles/min_height.md"
|
||||
- "styles/min_width.md"
|
||||
- "styles/offset.md"
|
||||
- "styles/outline.md"
|
||||
- "styles/overflow.md"
|
||||
- "styles/padding.md"
|
||||
- "styles/scrollbar.md"
|
||||
- "styles/scrollbar_size.md"
|
||||
- "styles/scrollbar_gutter.md"
|
||||
- "styles/scrollbar_size.md"
|
||||
- "styles/scrollbar.md"
|
||||
- "styles/text_style.md"
|
||||
- "styles/tint.md"
|
||||
- "styles/visibility.md"
|
||||
@@ -64,10 +64,13 @@ nav:
|
||||
- Widgets: "/widgets/"
|
||||
- Reference:
|
||||
- "reference/app.md"
|
||||
- "reference/color.md"
|
||||
- "reference/dom_node.md"
|
||||
- "reference/events.md"
|
||||
- "reference/geometry.md"
|
||||
- "reference/widget.md"
|
||||
|
||||
|
||||
markdown_extensions:
|
||||
- admonition
|
||||
- def_list
|
||||
@@ -115,6 +118,7 @@ theme:
|
||||
plugins:
|
||||
|
||||
- search:
|
||||
- autorefs:
|
||||
- mkdocstrings:
|
||||
default_handler: python
|
||||
handlers:
|
||||
|
||||
@@ -12,7 +12,18 @@ class ActionError(Exception):
|
||||
re_action_params = re.compile(r"([\w\.]+)(\(.*?\))")
|
||||
|
||||
|
||||
def parse(action: str) -> tuple[str, tuple[Any, ...]]:
|
||||
def parse(action: str) -> tuple[str, tuple[object, ...]]:
|
||||
"""Parses an action string.
|
||||
|
||||
Args:
|
||||
action (str): String containing action.
|
||||
|
||||
Raises:
|
||||
ActionError: If the action has invalid syntax.
|
||||
|
||||
Returns:
|
||||
tuple[str, tuple[object, ...]]: Action name and parameters
|
||||
"""
|
||||
params_match = re_action_params.match(action)
|
||||
if params_match is not None:
|
||||
action_name, action_params_str = params_match.groups()
|
||||
|
||||
@@ -39,16 +39,22 @@ class HLS(NamedTuple):
|
||||
"""A color in HLS format."""
|
||||
|
||||
h: float
|
||||
"""Hue"""
|
||||
l: float
|
||||
"""Lightness"""
|
||||
s: float
|
||||
"""Saturation"""
|
||||
|
||||
|
||||
class HSV(NamedTuple):
|
||||
"""A color in HSV format."""
|
||||
|
||||
h: float
|
||||
"""Hue"""
|
||||
s: float
|
||||
"""Saturation"""
|
||||
v: float
|
||||
"""Value"""
|
||||
|
||||
|
||||
class Lab(NamedTuple):
|
||||
@@ -103,9 +109,13 @@ class Color(NamedTuple):
|
||||
"""A class to represent a single RGB color with alpha."""
|
||||
|
||||
r: int
|
||||
"""Red component (0-255)"""
|
||||
g: int
|
||||
"""Green component (0-255)"""
|
||||
b: int
|
||||
"""Blue component (0-255)"""
|
||||
a: float = 1.0
|
||||
"""Alpha component (0-1)"""
|
||||
|
||||
@classmethod
|
||||
def from_rich_color(cls, rich_color: RichColor) -> Color:
|
||||
@@ -146,12 +156,22 @@ class Color(NamedTuple):
|
||||
|
||||
@property
|
||||
def is_transparent(self) -> bool:
|
||||
"""Check if the color is transparent, i.e. has 0 alpha."""
|
||||
"""Check if the color is transparent, i.e. has 0 alpha.
|
||||
|
||||
Returns:
|
||||
bool: True if transparent, otherwise False.
|
||||
|
||||
"""
|
||||
return self.a == 0
|
||||
|
||||
@property
|
||||
def clamped(self) -> Color:
|
||||
"""Get a color with all components saturated to maximum and minimum values."""
|
||||
"""Get a color with all components saturated to maximum and minimum values.
|
||||
|
||||
Returns:
|
||||
Color: A color object.
|
||||
|
||||
"""
|
||||
r, g, b, a = self
|
||||
_clamp = clamp
|
||||
color = Color(
|
||||
@@ -164,7 +184,11 @@ class Color(NamedTuple):
|
||||
|
||||
@property
|
||||
def rich_color(self) -> RichColor:
|
||||
"""This color encoded in Rich's Color class."""
|
||||
"""This color encoded in Rich's Color class.
|
||||
|
||||
Returns:
|
||||
RichColor: A color object as used by Rich.
|
||||
"""
|
||||
r, g, b, _a = self
|
||||
return RichColor(
|
||||
f"#{r:02x}{g:02x}{b:02x}", _TRUECOLOR, None, ColorTriplet(r, g, b)
|
||||
@@ -172,25 +196,43 @@ class Color(NamedTuple):
|
||||
|
||||
@property
|
||||
def normalized(self) -> tuple[float, float, float]:
|
||||
"""A tuple of the color components normalized to between 0 and 1."""
|
||||
"""A tuple of the color components normalized to between 0 and 1.
|
||||
|
||||
Returns:
|
||||
tuple[float, float, float]: Normalized components.
|
||||
|
||||
"""
|
||||
r, g, b, _a = self
|
||||
return (r / 255, g / 255, b / 255)
|
||||
|
||||
@property
|
||||
def rgb(self) -> tuple[int, int, int]:
|
||||
"""Get just the red, green, and blue components."""
|
||||
"""Get just the red, green, and blue components.
|
||||
|
||||
Returns:
|
||||
tuple[int, int, int]: Color components
|
||||
"""
|
||||
r, g, b, _ = self
|
||||
return (r, g, b)
|
||||
|
||||
@property
|
||||
def hls(self) -> HLS:
|
||||
"""Get the color as HLS."""
|
||||
"""Get the color as HLS.
|
||||
|
||||
Returns:
|
||||
HLS:
|
||||
"""
|
||||
r, g, b = self.normalized
|
||||
return HLS(*rgb_to_hls(r, g, b))
|
||||
|
||||
@property
|
||||
def brightness(self) -> float:
|
||||
"""Get the human perceptual brightness."""
|
||||
"""Get the human perceptual brightness.
|
||||
|
||||
Returns:
|
||||
float: Brightness value (0-1).
|
||||
|
||||
"""
|
||||
r, g, b = self.normalized
|
||||
brightness = (299 * r + 587 * g + 114 * b) / 1000
|
||||
return brightness
|
||||
|
||||
@@ -52,11 +52,7 @@ class NoParent(Exception):
|
||||
|
||||
@rich.repr.auto
|
||||
class DOMNode(MessagePump):
|
||||
"""A node in a hierarchy of things forming the UI.
|
||||
|
||||
Nodes are mountable and may be styled with CSS.
|
||||
|
||||
"""
|
||||
"""The base class for object that can be in the Textual DOM (App and Widget)"""
|
||||
|
||||
# Custom CSS
|
||||
CSS: ClassVar[str] = ""
|
||||
@@ -285,6 +281,12 @@ class DOMNode(MessagePump):
|
||||
|
||||
@property
|
||||
def classes(self) -> frozenset[str]:
|
||||
"""A frozenset of the current classes set on the widget.
|
||||
|
||||
Returns:
|
||||
frozenset[str]: Set of class names.
|
||||
|
||||
"""
|
||||
return frozenset(self._classes)
|
||||
|
||||
@property
|
||||
@@ -312,7 +314,10 @@ class DOMNode(MessagePump):
|
||||
@property
|
||||
def display(self) -> bool:
|
||||
"""
|
||||
Returns: ``True`` if this DOMNode is displayed (``display != "none"``), ``False`` otherwise.
|
||||
Check if this widget should display or note.
|
||||
|
||||
Returns:
|
||||
bool: ``True`` if this DOMNode is displayed (``display != "none"``) otherwise ``False`` .
|
||||
"""
|
||||
return self.styles.display != "none" and not (self._closing or self._closed)
|
||||
|
||||
@@ -484,7 +489,12 @@ class DOMNode(MessagePump):
|
||||
|
||||
@property
|
||||
def displayed_children(self) -> list[DOMNode]:
|
||||
"""The children which don't have display: none set."""
|
||||
"""The children which don't have display: none set.
|
||||
|
||||
Returns:
|
||||
list[DOMNode]: Children of this widget which will be displayed.
|
||||
|
||||
"""
|
||||
return [child for child in self.children if child.display]
|
||||
|
||||
def get_pseudo_classes(self) -> Iterable[str]:
|
||||
|
||||
@@ -141,13 +141,22 @@ class Size(NamedTuple):
|
||||
|
||||
@property
|
||||
def region(self) -> Region:
|
||||
"""Get a region of the same size."""
|
||||
"""Get a region of the same size.
|
||||
|
||||
Returns:
|
||||
Region: A region with the same size at (0, 0)
|
||||
|
||||
"""
|
||||
width, height = self
|
||||
return Region(0, 0, width, height)
|
||||
|
||||
@property
|
||||
def line_range(self) -> range:
|
||||
"""Get a range covering lines."""
|
||||
"""Get a range covering lines.
|
||||
|
||||
Returns:
|
||||
range:
|
||||
"""
|
||||
return range(self.height)
|
||||
|
||||
def __add__(self, other: object) -> Size:
|
||||
@@ -225,7 +234,7 @@ class Region(NamedTuple):
|
||||
y: int = 0
|
||||
"""Offset in the y-axis (vertical)"""
|
||||
width: int = 0
|
||||
"""The widget of the region"""
|
||||
"""The width of the region"""
|
||||
height: int = 0
|
||||
"""The height of the region"""
|
||||
|
||||
@@ -360,45 +369,85 @@ class Region(NamedTuple):
|
||||
|
||||
@property
|
||||
def right(self) -> int:
|
||||
"""Maximum X value (non inclusive)"""
|
||||
"""Maximum X value (non inclusive).
|
||||
|
||||
Returns:
|
||||
int: x coordinate
|
||||
|
||||
"""
|
||||
return self.x + self.width
|
||||
|
||||
@property
|
||||
def bottom(self) -> int:
|
||||
"""Maximum Y value (non inclusive)"""
|
||||
"""Maximum Y value (non inclusive).
|
||||
|
||||
Returns:
|
||||
int: y coordinate
|
||||
|
||||
"""
|
||||
return self.y + self.height
|
||||
|
||||
@property
|
||||
def area(self) -> int:
|
||||
"""Get the area within the region."""
|
||||
"""Get the area within the region.
|
||||
|
||||
Returns:
|
||||
int: area.
|
||||
|
||||
"""
|
||||
return self.width * self.height
|
||||
|
||||
@property
|
||||
def offset(self) -> Offset:
|
||||
"""Get the start point of the region."""
|
||||
"""Get the start point of the region.
|
||||
|
||||
Returns:
|
||||
Offset: Top left offset.
|
||||
|
||||
"""
|
||||
return Offset(self.x, self.y)
|
||||
|
||||
@property
|
||||
def bottom_left(self) -> Offset:
|
||||
"""Bottom left offset of the region."""
|
||||
"""Bottom left offset of the region.
|
||||
|
||||
Returns:
|
||||
Offset: Bottom left offset.
|
||||
|
||||
"""
|
||||
x, y, _width, height = self
|
||||
return Offset(x, y + height)
|
||||
|
||||
@property
|
||||
def top_right(self) -> Offset:
|
||||
"""Top right offset of the region."""
|
||||
"""Top right offset of the region.
|
||||
|
||||
Returns:
|
||||
Offset: Top right.
|
||||
|
||||
"""
|
||||
x, y, width, _height = self
|
||||
return Offset(x + width, y)
|
||||
|
||||
@property
|
||||
def bottom_right(self) -> Offset:
|
||||
"""Bottom right of the region."""
|
||||
"""Bottom right of the region.
|
||||
|
||||
Returns:
|
||||
Offset: Bottom right.
|
||||
|
||||
"""
|
||||
x, y, width, height = self
|
||||
return Offset(x + width, y + height)
|
||||
|
||||
@property
|
||||
def size(self) -> Size:
|
||||
"""Get the size of the region."""
|
||||
"""Get the size of the region.
|
||||
|
||||
Returns:
|
||||
Size: Size of the region.
|
||||
|
||||
"""
|
||||
return Size(self.width, self.height)
|
||||
|
||||
@property
|
||||
@@ -423,7 +472,12 @@ class Region(NamedTuple):
|
||||
|
||||
@property
|
||||
def reset_offset(self) -> Region:
|
||||
"""An region of the same size at (0, 0)."""
|
||||
"""An region of the same size at (0, 0).
|
||||
|
||||
Returns:
|
||||
Region: reset region.
|
||||
|
||||
"""
|
||||
_, _, width, height = self
|
||||
return Region(0, 0, width, height)
|
||||
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Collection
|
||||
|
||||
from rich.console import RenderableType
|
||||
|
||||
|
||||
from .geometry import Region, Size
|
||||
from .widget import Widget
|
||||
|
||||
|
||||
class ScrollView(Widget):
|
||||
"""
|
||||
A base class for a Widget that handles it's own scrolling (i.e. doesn't rely
|
||||
on the compositor to render children).
|
||||
|
||||
"""
|
||||
|
||||
CSS = """
|
||||
|
||||
ScrollView {
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, name: str | None = None, id: str | None = None, classes: str | None = None
|
||||
) -> None:
|
||||
super().__init__(name=name, id=id, classes=classes)
|
||||
|
||||
@property
|
||||
def is_scrollable(self) -> bool:
|
||||
"""Always scrollable."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_transparent(self) -> bool:
|
||||
"""Not transparent, i.e. renders something."""
|
||||
return False
|
||||
|
||||
def on_mount(self):
|
||||
self._refresh_scrollbars()
|
||||
|
||||
def get_content_width(self, container: Size, viewport: Size) -> int:
|
||||
"""Gets the width of the content area.
|
||||
|
||||
Args:
|
||||
container (Size): Size of the container (immediate parent) widget.
|
||||
viewport (Size): Size of the viewport.
|
||||
|
||||
Returns:
|
||||
int: The optimal width of the content.
|
||||
"""
|
||||
return self.virtual_size.width
|
||||
|
||||
def get_content_height(self, container: Size, viewport: Size, width: int) -> int:
|
||||
"""Gets the height (number of lines) in the content area.
|
||||
|
||||
Args:
|
||||
container (Size): Size of the container (immediate parent) widget.
|
||||
viewport (Size): Size of the viewport.
|
||||
width (int): Width of renderable.
|
||||
|
||||
Returns:
|
||||
int: The height of the content.
|
||||
"""
|
||||
return self.virtual_size.height
|
||||
|
||||
def size_updated(
|
||||
self, size: Size, virtual_size: Size, container_size: Size
|
||||
) -> None:
|
||||
"""Called when size is updated.
|
||||
|
||||
Args:
|
||||
size (Size): New size.
|
||||
virtual_size (Size): New virtual size.
|
||||
container_size (Size): New container size.
|
||||
"""
|
||||
virtual_size = self.virtual_size
|
||||
if self._size != size:
|
||||
self._size = size
|
||||
self._container_size = container_size
|
||||
|
||||
self._refresh_scrollbars()
|
||||
width, height = self.container_size
|
||||
if self.show_vertical_scrollbar:
|
||||
self.vertical_scrollbar.window_virtual_size = virtual_size.height
|
||||
self.vertical_scrollbar.window_size = height
|
||||
if self.show_horizontal_scrollbar:
|
||||
self.horizontal_scrollbar.window_virtual_size = virtual_size.width
|
||||
self.horizontal_scrollbar.window_size = width
|
||||
|
||||
self.scroll_x = self.validate_scroll_x(self.scroll_x)
|
||||
self.scroll_y = self.validate_scroll_y(self.scroll_y)
|
||||
self.refresh(layout=False)
|
||||
self.call_later(self.scroll_to, self.scroll_x, self.scroll_y)
|
||||
|
||||
def render(self) -> RenderableType:
|
||||
"""Render the scrollable region (if `render_lines` is not implemented).
|
||||
|
||||
Returns:
|
||||
RenderableType: Renderable object.
|
||||
"""
|
||||
from rich.panel import Panel
|
||||
|
||||
return Panel(f"{self.scroll_offset} {self.show_vertical_scrollbar}")
|
||||
|
||||
def watch_scroll_x(self, new_value: float) -> None:
|
||||
"""Called when horizontal bar is scrolled."""
|
||||
self.horizontal_scrollbar.position = int(new_value)
|
||||
self.refresh(layout=False)
|
||||
|
||||
def watch_scroll_y(self, new_value: float) -> None:
|
||||
"""Called when vertical bar is scrolled."""
|
||||
self.vertical_scrollbar.position = int(new_value)
|
||||
self.refresh(layout=False)
|
||||
@@ -72,6 +72,10 @@ class RenderCache(NamedTuple):
|
||||
|
||||
@rich.repr.auto
|
||||
class Widget(DOMNode):
|
||||
"""
|
||||
A Widget is the base class for Textual widgets. Extent this class (or a sub-class) when defining your own widgets.
|
||||
|
||||
"""
|
||||
|
||||
CSS = """
|
||||
Widget{
|
||||
@@ -488,6 +492,11 @@ class Widget(DOMNode):
|
||||
|
||||
@property
|
||||
def scrollbar_gutter(self) -> Spacing:
|
||||
"""Spacing required to fit scrollbar(s)
|
||||
|
||||
Returns:
|
||||
Spacing: Scrollbar gutter spacing.
|
||||
"""
|
||||
gutter = Spacing(
|
||||
0, self.scrollbar_size_vertical, self.scrollbar_size_horizontal, 0
|
||||
)
|
||||
@@ -495,39 +504,73 @@ class Widget(DOMNode):
|
||||
|
||||
@property
|
||||
def gutter(self) -> Spacing:
|
||||
"""Spacing for padding / border / scrollbars."""
|
||||
"""Spacing for padding / border / scrollbars.
|
||||
|
||||
Returns:
|
||||
Spacing: Additional spacing around content area.
|
||||
|
||||
"""
|
||||
return self.styles.gutter + self.scrollbar_gutter
|
||||
|
||||
@property
|
||||
def size(self) -> Size:
|
||||
"""The size of the content area."""
|
||||
"""The size of the content area.
|
||||
|
||||
Returns:
|
||||
Size: Content area size.
|
||||
"""
|
||||
return self.content_region.size
|
||||
|
||||
@property
|
||||
def outer_size(self) -> Size:
|
||||
"""The size of the widget (including padding and border)."""
|
||||
"""The size of the widget (including padding and border).
|
||||
|
||||
Returns:
|
||||
Size: Outer size.
|
||||
"""
|
||||
return self._size
|
||||
|
||||
@property
|
||||
def container_size(self) -> Size:
|
||||
"""The size of the container (parent widget)."""
|
||||
"""The size of the container (parent widget).
|
||||
|
||||
Returns:
|
||||
Size: Container size.
|
||||
"""
|
||||
return self._container_size
|
||||
|
||||
@property
|
||||
def content_region(self) -> Region:
|
||||
"""Gets an absolute region containing the content (minus padding and border)."""
|
||||
"""Gets an absolute region containing the content (minus padding and border).
|
||||
|
||||
Returns:
|
||||
Region: Screen region that contains a widget's content.
|
||||
"""
|
||||
content_region = self.region.shrink(self.gutter)
|
||||
return content_region
|
||||
|
||||
@property
|
||||
def content_offset(self) -> Offset:
|
||||
"""An offset from the Widget origin where the content begins."""
|
||||
"""An offset from the Widget origin where the content begins.
|
||||
|
||||
Returns:
|
||||
Offset: Offset from widget's origin.
|
||||
|
||||
"""
|
||||
x, y = self.gutter.top_left
|
||||
return Offset(x, y)
|
||||
|
||||
@property
|
||||
def region(self) -> Region:
|
||||
"""The region occupied by this widget, relative to the Screen."""
|
||||
"""The region occupied by this widget, relative to the Screen.
|
||||
|
||||
Raises:
|
||||
NoScreen: If there is no screen.
|
||||
errors.NoWidget: If the widget is not on the screen.
|
||||
|
||||
Returns:
|
||||
Region: Region within screen occupied by widget.
|
||||
"""
|
||||
try:
|
||||
return self.screen.find_widget(self).region
|
||||
except NoScreen:
|
||||
@@ -596,7 +639,12 @@ class Widget(DOMNode):
|
||||
|
||||
@property
|
||||
def console(self) -> Console:
|
||||
"""Get the current console."""
|
||||
"""Get the current console.
|
||||
|
||||
Returns:
|
||||
Console: A Rich console object.
|
||||
|
||||
"""
|
||||
return active_app.get().console
|
||||
|
||||
@property
|
||||
@@ -631,7 +679,12 @@ class Widget(DOMNode):
|
||||
|
||||
@property
|
||||
def layer(self) -> str:
|
||||
"""Get the name of this widgets layer."""
|
||||
"""Get the name of this widgets layer.
|
||||
|
||||
Returns:
|
||||
str: Name of layer.
|
||||
|
||||
"""
|
||||
return self.styles.layer or "default"
|
||||
|
||||
@property
|
||||
|
||||
Reference in New Issue
Block a user