merge
412
README.md
@@ -1,374 +1,112 @@
|
||||
# Textual
|
||||
|
||||

|
||||

|
||||
|
||||
Textual is a TUI (Text User Interface) framework for Python inspired by modern web development. Currently a **Work in Progress**.
|
||||
Textual is a Python framework for creating interactive applications that run in your terminal.
|
||||
|
||||
<details>
|
||||
<summary> 🎬 Code browser </summary>
|
||||
<hr>
|
||||
|
||||
This is the [code_browser.py](./examples/code_browser.py) example which clocks in at 61 lines (*including* docstrings and blank lines).
|
||||
|
||||
https://user-images.githubusercontent.com/554369/189394703-364b5caa-97e0-45db-907d-7b1620d6411f.mov
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
> ⚠ **NOTE:** We ([Textualize.io](https://www.textualize.io)) are hard at work on the **css** branch. We will be maintain the 0.1.0 branch for the near future but may not be able to accept API changes. If you would like to contribute code via a PR, please raise a discussion first, to avoid disapointment.
|
||||
## About
|
||||
|
||||
Textual adds interactivity to [Rich](https://github.com/Textualize/rich) with a Python API inspired by modern development development.
|
||||
|
||||
Follow [@willmcgugan](https://twitter.com/willmcgugan) for progress updates, or post in Discussions if you have any requests / suggestions.
|
||||
|
||||
[](https://gitter.im/textual-ui/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
On modern terminal software (installed by default on most systems), Textual apps can use **16.7 million** colors with mouse support and smooth flicker-free animation. A powerful layout engine and re-usable components makes it possible to build apps that rival the desktop and web experience.
|
||||
|
||||
## Compatibility
|
||||
|
||||
Textual currently runs on **MacOS / Linux / Window**.
|
||||
Textual runs on Linux, MacOS, and Windows. Textual requires Python 3.7 or above.
|
||||
|
||||
## How it works
|
||||
## Installing
|
||||
|
||||
Textual uses [Rich](https://github.com/willmcgugan/rich) to render rich text, so anything that Rich can render may be used in Textual.
|
||||
|
||||
Event handling in Textual is asynchronous (using `async` and `await` keywords). Widgets (UI components) can independently update and communicate with each other via message passing.
|
||||
|
||||
Textual has more in common with modern web development than it does with [curses](<https://en.wikipedia.org/wiki/Curses_(programming_library)>); layout is done with CSS grid and (soon) the theme may be customized with CSS. Other techniques are borrowed from JS frameworks such as Vue and React.
|
||||
|
||||
## Installation
|
||||
|
||||
You can install Textual via pip (`pip install textual`), or by checking out the repo and installing with [poetry](https://python-poetry.org/docs/).
|
||||
Install Textual via pip:
|
||||
|
||||
```
|
||||
poetry install
|
||||
pip install textual[dev]
|
||||
```
|
||||
|
||||
Once installed you can run the following command for a quick test, or see examples (below):
|
||||
The addition of `[dev]` installs Textual development tools.
|
||||
|
||||
```
|
||||
python -m textual.app
|
||||
|
||||
## Reference commands
|
||||
|
||||
The `textual` command has a few sub-commands to preview Textual styles.
|
||||
|
||||
<details>
|
||||
<summary> 🎬 Easing reference </summary>
|
||||
<hr>
|
||||
|
||||
This is the *easing* reference which demonstrates the easing parameter on animation, with both movement and opacity. You can run it with the following command:
|
||||
|
||||
```bash
|
||||
textual easing
|
||||
```
|
||||
|
||||
Textual requires Python 3.7 or above.
|
||||
https://user-images.githubusercontent.com/554369/189485538-31e794ff-61d7-4faf-902a-6e90a9d76e5b.mov
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary> 🎬 Borders reference </summary>
|
||||
<hr>
|
||||
|
||||
This is the borders reference which demonstrates some of the borders styles in Textual. You can run it with the following command:
|
||||
|
||||
```bash
|
||||
textual borders
|
||||
```
|
||||
|
||||
|
||||
https://user-images.githubusercontent.com/554369/189485735-cb2b4135-caee-46d7-a118-66cd7ed9eef5.mov
|
||||
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
## Examples
|
||||
|
||||
Until I've written the documentation, the [examples](https://github.com/willmcgugan/textual/tree/main/examples/) may be the best way to learn Textual.
|
||||
The Textual repository comes with a number of examples you can experiment with or use as a template for your own projects.
|
||||
|
||||
You can see some of these examples in action in the [Developer Video Log](#developer-video-log).
|
||||
<details>
|
||||
<summary> 📷 Calculator </summary>
|
||||
<hr>
|
||||
|
||||
- [animation.py](https://github.com/willmcgugan/textual/tree/main/examples/animation.py) Demonstration of 60fps animation easing function
|
||||
- [calculator.py](https://github.com/willmcgugan/textual/tree/main/examples/calculator.py) A "clone" of the MacOS calculator using Grid layout
|
||||
- [code_viewer.py](https://github.com/willmcgugan/textual/tree/main/examples/code_viewer.py) A demonstration of a tree view which loads syntax highlighted code
|
||||
- [grid.py](https://github.com/willmcgugan/textual/tree/main/examples/calculator.py) A simple demonstration of adding widgets in a Grid layout
|
||||
- [grid_auto.py](https://github.com/willmcgugan/textual/tree/main/examples/grid_auto.py) A demonstration of automatic Grid layout
|
||||
- [simple.py](https://github.com/willmcgugan/textual/tree/main/examples/simple.py) A very simple Textual app with scrolling Markdown view
|
||||
This is [calculator.py](./examples/calculator.py) which demonstrates Textual grid layouts.
|
||||
|
||||
## Building Textual applications
|
||||

|
||||
</details>
|
||||
|
||||
_This guide is a work in progress_
|
||||
<details>
|
||||
<summary> 📷 Code browser </summary>
|
||||
<hr>
|
||||
|
||||
Let's look at the simplest Textual app which does _something_:
|
||||
This is [code_browser.py](./examples/code_browser.py) which demonstrates the directory tree widget.
|
||||
|
||||
```python
|
||||
from textual.app import App
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
class Beeper(App):
|
||||
def on_key(self):
|
||||
self.console.bell()
|
||||
|
||||
<details>
|
||||
<summary> 📷 Stopwatch </summary>
|
||||
<hr>
|
||||
|
||||
Beeper.run()
|
||||
```
|
||||
This is the Stopwatch example from the tutorial.
|
||||
|
||||
Here we can see a textual app with a single `on_key` method which will handle key events. Pressing any key will result in playing the terminal bell (generally an irritating beep). Hit Ctrl+C to exit.
|
||||
### Light theme
|
||||
|
||||
Event handlers in Textual are defined by convention, not by inheritance (there's no base class with all the handlers defined). Each event has a `name` attribute which for the key event is simply `"key"`. Textual will call the method named `on_<event.name>` if it exists.
|
||||

|
||||
|
||||
Let's look at a _slightly_ more interesting example:
|
||||
### Dark theme
|
||||
|
||||
```python
|
||||
from textual.app import App
|
||||

|
||||
|
||||
|
||||
class ColorChanger(App):
|
||||
def on_key(self, event):
|
||||
if event.key.isdigit():
|
||||
self.background = f"on color({event.key})"
|
||||
|
||||
|
||||
ColorChanger.run(log_path="textual.log")
|
||||
```
|
||||
|
||||
You'll notice that the `on_key` method above contains an additional `event` parameter which wasn't present on the beeper example. If the `event` argument is present, Textual will call the handler with an event object. Every event has an associated handler object, in this case it is a KeyEvent which contains additional information regarding which key was pressed.
|
||||
|
||||
The key event handler above will set the background attribute if you press the keys 0-9, which turns the terminal to the corresponding [ansi color](https://rich.readthedocs.io/en/latest/appendix/colors.html).
|
||||
|
||||
Note that we didn't need to explicitly refresh the screen or draw anything. Setting the `background` attribute to a [Rich style](https://rich.readthedocs.io/en/latest/style.html) is enough for Textual to update the visuals. This is an example of _reactivity_ in Textual. To make changes to the terminal interface you modify the _state_ and let Textual update the UI.
|
||||
|
||||
## Widgets
|
||||
|
||||
To make more interesting apps you will need to make use of _widgets_, which are independent user interface elements. Textual comes with a (growing) library of widgets, but you can develop your own.
|
||||
|
||||
Let's look at an app which contains widgets. We will be using the built-in `Placeholder` widget which you can use to design application layouts before you implement the real content.
|
||||
|
||||
```python
|
||||
from textual.app import App
|
||||
from textual.widgets import Placeholder
|
||||
|
||||
|
||||
class SimpleApp(App):
|
||||
|
||||
async def on_mount(self) -> None:
|
||||
await self.view.dock(Placeholder(), edge="left", size=40)
|
||||
await self.view.dock(Placeholder(), Placeholder(), edge="top")
|
||||
|
||||
|
||||
SimpleApp.run(log_path="textual.log")
|
||||
```
|
||||
|
||||
This app contains a single event handler `on_mount`. The mount event is sent when the app or widget is ready to start processing events, and is typically used for initialization. You may have noticed that `on_mount` is an `async` function. Since Textual is an asynchronous framework we will need this if we need to call most other methods.
|
||||
|
||||
The `on_mount` method makes two calls to `self.view.dock` which adds widgets to the terminal.
|
||||
|
||||
Here's the first line in the mount handler:
|
||||
|
||||
```python
|
||||
await self.view.dock(Placeholder(), edge="left", size=40)
|
||||
```
|
||||
|
||||
Note this method is asynchronous like almost all API methods in Textual. We are awaiting `self.view.dock` which takes a newly constructed Placeholder widget, and docks it on to the `"left"` edge of the terminal with a size of 40 characters. In a real app you might use this to display a side-bar.
|
||||
|
||||
The following line is similar:
|
||||
|
||||
```python
|
||||
await self.view.dock(Placeholder(), Placeholder(), edge="top")
|
||||
```
|
||||
|
||||
You will notice that this time we are docking _two_ Placeholder objects onto the `"top"` edge. We haven't set an explicit size this time so Textual will divide the remaining size amongst the two new widgets.
|
||||
|
||||
The last line calls the `run` class method in the usual way, but with an argument we haven't seen before: `log_path="textual.log"` tells Textual to write log information to the given file. You can tail textual.log to see events being processed and other debug information.
|
||||
|
||||
If you run the above example, you will see something like the following:
|
||||
|
||||

|
||||
|
||||
If you move the mouse over the terminal you will notice that widgets receive mouse events. You can click any of the placeholders to give it input focus.
|
||||
|
||||
The dock layout feature is very flexible, but for more sophisticated layouts we can use the grid API. See the [calculator.py](https://github.com/willmcgugan/textual/blob/main/examples/calculator.py) example which makes use of Grid.
|
||||
|
||||
### Creating Widgets
|
||||
|
||||
You can create your own widgets by subclassing the `textual.widget.Widget` class and implementing a `render()` method which should return anything that can be rendered with [Rich](https://rich.readthedocs.io/en/latest/introduction.html), including a plain string which will be interpreted as [console markup](https://rich.readthedocs.io/en/latest/markup.html).
|
||||
|
||||
Let's look at an example with a custom widget:
|
||||
|
||||
```python
|
||||
from rich.panel import Panel
|
||||
|
||||
from textual.app import App
|
||||
from textual.reactive import Reactive
|
||||
from textual.widget import Widget
|
||||
|
||||
|
||||
class Hover(Widget):
|
||||
|
||||
mouse_over = Reactive(False)
|
||||
|
||||
def render(self) -> Panel:
|
||||
return Panel("Hello [b]World[/b]", style=("on red" if self.mouse_over else ""))
|
||||
|
||||
def on_enter(self) -> None:
|
||||
self.mouse_over = True
|
||||
|
||||
def on_leave(self) -> None:
|
||||
self.mouse_over = False
|
||||
|
||||
|
||||
class HoverApp(App):
|
||||
"""Demonstrates custom widgets"""
|
||||
|
||||
async def on_mount(self) -> None:
|
||||
hovers = (Hover() for _ in range(10))
|
||||
await self.view.dock(*hovers, edge="top")
|
||||
|
||||
|
||||
HoverApp.run(log_path="textual.log")
|
||||
```
|
||||
|
||||
The `Hover` class is a custom widget which displays a panel containing the classic text "Hello World". The first line in the Hover class may seem a little mysterious at this point:
|
||||
|
||||
```python
|
||||
mouse_over = Reactive(False)
|
||||
```
|
||||
|
||||
This adds a `mouse_over` attribute to your class which is a bool with a default of `False`. Adding attributes like this makes them _reactive_: any changes will result in the widget updating.
|
||||
|
||||
The following `render()` method is where you define how the widget should be displayed. In the Hover widget we return a [Panel](https://rich.readthedocs.io/en/latest/panel.html) containing rich text with a background that changes depending on the value of `mouse_over`. The goal here is to add a mouse hover effect to the widget, which we can achieve by handling two events: `Enter` and `Leave`. These events are sent when the mouse enters or leaves the widget.
|
||||
|
||||
Here are the two event handlers again:
|
||||
|
||||
```python
|
||||
def on_enter(self) -> None:
|
||||
self.mouse_over = True
|
||||
|
||||
def on_leave(self) -> None:
|
||||
self.mouse_over = False
|
||||
```
|
||||
|
||||
Both event handlers set the `mouse_over` attribute which will result in the widget's `render()` method being called.
|
||||
|
||||
The `HoverApp` has a `on_mount` handler which creates 10 Hover widgets and docks them on the top edge to create a vertical stack:
|
||||
|
||||
```python
|
||||
async def on_mount(self) -> None:
|
||||
hovers = (Hover() for _ in range(10))
|
||||
await self.view.dock(*hovers, edge="top")
|
||||
```
|
||||
|
||||
If you run this script you will see something like the following:
|
||||
|
||||

|
||||
|
||||
If you move your mouse over the terminal you should see that the widget under the mouse cursor changes to a red background.
|
||||
|
||||
### Actions and key bindings
|
||||
|
||||
Actions in Textual are white-listed functions that may be bound to keys. Let's look at a trivial example of binding a key to an action. Here is an app which exits when we hit the Q key:
|
||||
|
||||
```python
|
||||
from textual.app import App
|
||||
|
||||
|
||||
class Quitter(App):
|
||||
async def on_load(self, event):
|
||||
await self.bind("q", "quit")
|
||||
|
||||
|
||||
Quitter.run()
|
||||
```
|
||||
|
||||
If you run this you will get a blank terminal which will return to the prompt when you press Q.
|
||||
|
||||
Binding is done in the Load event handler. The `bind` method takes the key (in this case "q") and binds it to an action ("quit"). The quit action is built in to Textual and simply exits the app.
|
||||
|
||||
To define your own actions, add a method that begins with `action_`, which may take parameters. Let's create a simple action that changes the color of the terminal and binds keys to it:
|
||||
|
||||
```python
|
||||
from textual.app import App
|
||||
|
||||
|
||||
class Colorizer(App):
|
||||
|
||||
async def on_load(self, event):
|
||||
await self.bind("r", "color('red')")
|
||||
await self.bind("g", "color('green')")
|
||||
await self.bind("b", "color('blue')")
|
||||
|
||||
async def action_color(self, color:str) -> None:
|
||||
self.background = f"on {color}"
|
||||
|
||||
|
||||
Colorizer.run()
|
||||
```
|
||||
|
||||
If you run this app you can hit the keys R, G, or B to change the color of the background.
|
||||
|
||||
In the `on_load` method we have bound the keys R, G, and B to the `color` action with a single parameter. When you press any of these three keys Textual will call the method `action_color` with the appropriate parameter.
|
||||
|
||||
You could be forgiven for thinking that `"color('red')"` is Python code which Textual evaluates. This is not the case. The action strings are parsed and may not include expressions or arbitrary code. The reason that strings are used over a callable is that (in a future update) key bindings may be loaded from a configuration file.
|
||||
|
||||
### More on Events
|
||||
|
||||
_TODO_
|
||||
|
||||
### Watchers
|
||||
|
||||
_TODO_
|
||||
|
||||
### Animation
|
||||
|
||||
_TODO_
|
||||
|
||||
### Timers and Intervals
|
||||
|
||||
Textual has a `set_timer` and a `set_interval` method which work much like their Javascript counterparts. The `set_timer` method will invoke a callable after a given period of time, and `set_interval` will invoke a callable repeatedly. Unlike Javascript these methods expect the time to be in seconds (_not_ milliseconds).
|
||||
|
||||
Let's create a simple terminal based clock with the `set_interval` method:
|
||||
|
||||
```python
|
||||
from datetime import datetime
|
||||
|
||||
from rich.align import Align
|
||||
|
||||
from textual.app import App
|
||||
from textual.widget import Widget
|
||||
|
||||
|
||||
class Clock(Widget):
|
||||
def on_mount(self):
|
||||
self.set_interval(1, self.refresh)
|
||||
|
||||
def render(self):
|
||||
time = datetime.now().strftime("%c")
|
||||
return Align.center(time, vertical="middle")
|
||||
|
||||
|
||||
class ClockApp(App):
|
||||
async def on_mount(self):
|
||||
await self.view.dock(Clock())
|
||||
|
||||
|
||||
ClockApp.run()
|
||||
|
||||
```
|
||||
|
||||
If you run this app you will see the current time in the center of the terminal until you hit Ctrl+C.
|
||||
|
||||
The Clock widget displays the time using [rich.align.Align](https://rich.readthedocs.io/en/latest/reference/align.html) to position it in the center. In the clock's Mount handler there is the following call to `set_interval`:
|
||||
|
||||
```python
|
||||
self.set_interval(1, self.refresh)
|
||||
```
|
||||
|
||||
This tells Textual to call a function (in this case `self.refresh` which updates the widget) once a second. When a widget is refreshed it calls `Clock.render` again to display the latest time.
|
||||
|
||||
## Developer Video Log
|
||||
|
||||
Since Textual is a visual medium, I'll be documenting new features and milestones here.
|
||||
|
||||
### Update 1 - Basic scrolling
|
||||
|
||||
[](http://www.youtube.com/watch?v=zNW7U36GHlU)
|
||||
|
||||
### Update 2 - Keyboard toggle
|
||||
|
||||
[](http://www.youtube.com/watch?v=bTYeFOVNXDI)
|
||||
|
||||
### Update 3 - New scrollbars and smooth scrolling
|
||||
|
||||
[](http://www.youtube.com/watch?v=4LVl3ClrXIs)
|
||||
|
||||
### Update 4 - Animation system with easing function
|
||||
|
||||
Now with a system to animate changes to values, going from the initial to the final value in small increments over time . Here applied to the scroll position. The animation system supports CSS like _easing functions_. You may be able to tell from the video that the page up / down keys cause the window to first speed up and then slow down.
|
||||
|
||||
[](http://www.youtube.com/watch?v=k2VwOp1YbSk)
|
||||
|
||||
### Update 5 - New Layout system
|
||||
|
||||
A new update system allows for overlapping layers. Animation is now synchronized with the display which makes it very smooth!
|
||||
|
||||
[](http://www.youtube.com/watch?v=XxRnfx2WYRw)
|
||||
|
||||
### Update 6 - New Layout API
|
||||
|
||||
New version (0.1.4) with API updates and the new layout system.
|
||||
|
||||
[](http://www.youtube.com/watch?v=jddccDuVd3E)
|
||||
|
||||
### Update 7 - New Grid Layout
|
||||
|
||||
**11 July 2021**
|
||||
|
||||
Added a new layout system modelled on CSS grid. The example demonstrates how once created a grid will adapt to the available space.
|
||||
|
||||
[](http://www.youtube.com/watch?v=Zh9CEvu73jc)
|
||||
|
||||
## Update 8 - Tree control and scroll views
|
||||
|
||||
**6 Aug 2021**
|
||||
|
||||
Added a tree control and refactored the renderer to allow for widgets within a scrollable view
|
||||
|
||||
[](http://www.youtube.com/watch?v=J-dzzD6NQJ4)
|
||||
</details>
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
App > Screen {
|
||||
|
||||
background: $surface;
|
||||
color: $text-surface;
|
||||
color: $text;
|
||||
layers: base sidebar;
|
||||
|
||||
color: $text-background;
|
||||
color: $text;
|
||||
background: $background;
|
||||
layout: vertical;
|
||||
|
||||
@@ -53,7 +53,7 @@ DataTable {
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
color: $text-panel;
|
||||
color: $text;
|
||||
background: $panel;
|
||||
dock: left;
|
||||
width: 30;
|
||||
@@ -71,7 +71,7 @@ DataTable {
|
||||
#sidebar .title {
|
||||
height: 1;
|
||||
background: $primary-background-darken-1;
|
||||
color: $text-primary-background-darken-1;
|
||||
color: $text-muted;
|
||||
border-right: wide $background;
|
||||
content-align: center middle;
|
||||
}
|
||||
@@ -79,14 +79,14 @@ DataTable {
|
||||
#sidebar .user {
|
||||
height: 8;
|
||||
background: $panel-darken-1;
|
||||
color: $text-panel-darken-1;
|
||||
color: $text-muted;
|
||||
border-right: wide $background;
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
#sidebar .content {
|
||||
background: $panel-darken-2;
|
||||
color: $text-surface;
|
||||
color: $text;
|
||||
border-right: wide $background;
|
||||
content-align: center middle;
|
||||
}
|
||||
@@ -100,7 +100,7 @@ Tweet {
|
||||
margin: 0 2;
|
||||
|
||||
background: $panel;
|
||||
color: $text-panel;
|
||||
color: $text;
|
||||
layout: vertical;
|
||||
/* border: outer $primary; */
|
||||
padding: 1;
|
||||
@@ -130,13 +130,13 @@ Tweet {
|
||||
TweetHeader {
|
||||
height:1;
|
||||
background: $accent;
|
||||
color: $text-accent
|
||||
color: $text
|
||||
}
|
||||
|
||||
TweetBody {
|
||||
width: 100%;
|
||||
background: $panel;
|
||||
color: $text-panel;
|
||||
color: $text;
|
||||
height: auto;
|
||||
padding: 0 1 0 0;
|
||||
}
|
||||
@@ -147,7 +147,7 @@ Tweet.scroll-horizontal TweetBody {
|
||||
|
||||
.button {
|
||||
background: $accent;
|
||||
color: $text-accent;
|
||||
color: $text;
|
||||
width:20;
|
||||
height: 3;
|
||||
/* border-top: hidden $accent-darken-3; */
|
||||
@@ -163,7 +163,7 @@ Tweet.scroll-horizontal TweetBody {
|
||||
|
||||
.button:hover {
|
||||
background: $accent-lighten-1;
|
||||
color: $text-accent-lighten-1;
|
||||
color: $text-disabled;
|
||||
width: 20;
|
||||
height: 3;
|
||||
border: tall $accent-darken-1;
|
||||
@@ -175,7 +175,7 @@ Tweet.scroll-horizontal TweetBody {
|
||||
}
|
||||
|
||||
#footer {
|
||||
color: $text-accent;
|
||||
color: $text;
|
||||
background: $accent;
|
||||
height: 1;
|
||||
|
||||
@@ -198,7 +198,7 @@ OptionItem {
|
||||
|
||||
OptionItem:hover {
|
||||
height: 3;
|
||||
color: $text-primary;
|
||||
color: $text;
|
||||
background: $primary-darken-1;
|
||||
/* border-top: hkey $accent2-darken-3;
|
||||
border-bottom: hkey $accent2-darken-3; */
|
||||
@@ -210,7 +210,7 @@ Error {
|
||||
width: 100%;
|
||||
height:3;
|
||||
background: $error;
|
||||
color: $text-error;
|
||||
color: $text;
|
||||
border-top: tall $error-darken-2;
|
||||
border-bottom: tall $error-darken-2;
|
||||
|
||||
@@ -223,7 +223,7 @@ Warning {
|
||||
width: 100%;
|
||||
height:3;
|
||||
background: $warning;
|
||||
color: $text-warning-fade-1;
|
||||
color: $text-muted;
|
||||
border-top: tall $warning-darken-2;
|
||||
border-bottom: tall $warning-darken-2;
|
||||
|
||||
@@ -237,7 +237,7 @@ Success {
|
||||
height:auto;
|
||||
box-sizing: border-box;
|
||||
background: $success;
|
||||
color: $text-success-fade-1;
|
||||
color: $text-muted;
|
||||
|
||||
border-top: hkey $success-darken-2;
|
||||
border-bottom: hkey $success-darken-2;
|
||||
|
||||
@@ -33,7 +33,7 @@ Button {
|
||||
.started {
|
||||
text-style: bold;
|
||||
background: $success;
|
||||
color: $text-success;
|
||||
color: $text;
|
||||
}
|
||||
|
||||
.started TimeDisplay {
|
||||
|
||||
@@ -33,7 +33,7 @@ Button {
|
||||
.started {
|
||||
text-style: bold;
|
||||
background: $success;
|
||||
color: $text-success;
|
||||
color: $text;
|
||||
}
|
||||
|
||||
.started TimeDisplay {
|
||||
|
||||
@@ -14,30 +14,45 @@
|
||||
|
||||
App > Screen {
|
||||
|
||||
background: $surface;
|
||||
color: $text-surface;
|
||||
layers: sidebar;
|
||||
|
||||
color: $text-background;
|
||||
background: $background;
|
||||
color: $text;
|
||||
layers: base sidebar;
|
||||
layout: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#tree-container {
|
||||
overflow-y: auto;
|
||||
height: 20;
|
||||
margin: 1 2;
|
||||
background: $surface;
|
||||
padding: 1 2;
|
||||
}
|
||||
|
||||
DirectoryTree {
|
||||
padding: 0 1;
|
||||
height: auto;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
DataTable {
|
||||
/*border:heavy red;*/
|
||||
/* tint: 10% green; */
|
||||
/* text-opacity: 50%; */
|
||||
padding: 1;
|
||||
margin: 1 2;
|
||||
height: 12;
|
||||
height: 24;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
color: $text-panel;
|
||||
background: $panel;
|
||||
color: $text;
|
||||
dock: left;
|
||||
width: 30;
|
||||
margin-bottom: 1;
|
||||
offset-x: -100%;
|
||||
|
||||
transition: offset 500ms in_out_cubic;
|
||||
@@ -51,7 +66,7 @@ DataTable {
|
||||
#sidebar .title {
|
||||
height: 1;
|
||||
background: $primary-background-darken-1;
|
||||
color: $text-primary-background-darken-1;
|
||||
color: $text;
|
||||
border-right: wide $background;
|
||||
content-align: center middle;
|
||||
}
|
||||
@@ -59,35 +74,29 @@ DataTable {
|
||||
#sidebar .user {
|
||||
height: 8;
|
||||
background: $panel-darken-1;
|
||||
color: $text-panel-darken-1;
|
||||
color: $text;
|
||||
border-right: wide $background;
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
#sidebar .content {
|
||||
background: $panel-darken-2;
|
||||
color: $text-surface;
|
||||
color: $text;
|
||||
border-right: wide $background;
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
#header {
|
||||
color: $text-secondary-background;
|
||||
background: $secondary-background;
|
||||
height: 1;
|
||||
content-align: center middle;
|
||||
|
||||
dock: top;
|
||||
}
|
||||
|
||||
|
||||
Tweet {
|
||||
height:12;
|
||||
width: 100%;
|
||||
margin: 0 2;
|
||||
|
||||
|
||||
margin:0 2;
|
||||
background: $panel;
|
||||
color: $text-panel;
|
||||
color: $text;
|
||||
layout: vertical;
|
||||
/* border: outer $primary; */
|
||||
padding: 1;
|
||||
@@ -96,14 +105,15 @@ Tweet {
|
||||
/* scrollbar-gutter: stable; */
|
||||
align-horizontal: center;
|
||||
box-sizing: border-box;
|
||||
|
||||
}
|
||||
|
||||
|
||||
.scrollable {
|
||||
|
||||
overflow-x: auto;
|
||||
overflow-y: scroll;
|
||||
margin: 1 2;
|
||||
height: 20;
|
||||
height: 24;
|
||||
align-horizontal: center;
|
||||
layout: vertical;
|
||||
}
|
||||
@@ -117,13 +127,13 @@ Tweet {
|
||||
TweetHeader {
|
||||
height:1;
|
||||
background: $accent;
|
||||
color: $text-accent
|
||||
color: $text;
|
||||
}
|
||||
|
||||
TweetBody {
|
||||
width: 100%;
|
||||
background: $panel;
|
||||
color: $text-panel;
|
||||
color: $text;
|
||||
height: auto;
|
||||
padding: 0 1 0 0;
|
||||
}
|
||||
@@ -134,7 +144,7 @@ Tweet.scroll-horizontal TweetBody {
|
||||
|
||||
.button {
|
||||
background: $accent;
|
||||
color: $text-accent;
|
||||
color: $text;
|
||||
width:20;
|
||||
height: 3;
|
||||
/* border-top: hidden $accent-darken-3; */
|
||||
@@ -150,7 +160,7 @@ Tweet.scroll-horizontal TweetBody {
|
||||
|
||||
.button:hover {
|
||||
background: $accent-lighten-1;
|
||||
color: $text-accent-lighten-1;
|
||||
color: $text;
|
||||
width: 20;
|
||||
height: 3;
|
||||
border: tall $accent-darken-1;
|
||||
@@ -162,7 +172,7 @@ Tweet.scroll-horizontal TweetBody {
|
||||
}
|
||||
|
||||
#footer {
|
||||
color: $text-accent;
|
||||
color: $text;
|
||||
background: $accent;
|
||||
height: 1;
|
||||
|
||||
@@ -185,7 +195,7 @@ OptionItem {
|
||||
|
||||
OptionItem:hover {
|
||||
height: 3;
|
||||
color: $text-primary;
|
||||
color: $text;
|
||||
background: $primary-darken-1;
|
||||
/* border-top: hkey $accent2-darken-3;
|
||||
border-bottom: hkey $accent2-darken-3; */
|
||||
@@ -197,7 +207,7 @@ Error {
|
||||
width: 100%;
|
||||
height:3;
|
||||
background: $error;
|
||||
color: $text-error;
|
||||
color: $text;
|
||||
border-top: tall $error-darken-2;
|
||||
border-bottom: tall $error-darken-2;
|
||||
|
||||
@@ -210,7 +220,7 @@ Warning {
|
||||
width: 100%;
|
||||
height:3;
|
||||
background: $warning;
|
||||
color: $text-warning-fade-1;
|
||||
color: $text;
|
||||
border-top: tall $warning-darken-2;
|
||||
border-bottom: tall $warning-darken-2;
|
||||
|
||||
@@ -224,7 +234,7 @@ Success {
|
||||
height:auto;
|
||||
box-sizing: border-box;
|
||||
background: $success;
|
||||
color: $text-success-fade-1;
|
||||
color: $text;
|
||||
|
||||
border-top: hkey $success-darken-2;
|
||||
border-bottom: hkey $success-darken-2;
|
||||
|
||||
@@ -6,42 +6,55 @@ from rich.text import Text
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.reactive import Reactive
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Static, DataTable
|
||||
from textual.widgets import Static, DataTable, DirectoryTree, Header, Footer
|
||||
from textual.layout import Container
|
||||
|
||||
CODE = '''
|
||||
class Offset(NamedTuple):
|
||||
"""A point defined by x and y coordinates."""
|
||||
from __future__ import annotations
|
||||
|
||||
x: int = 0
|
||||
y: int = 0
|
||||
from typing import Iterable, TypeVar
|
||||
|
||||
@property
|
||||
def is_origin(self) -> bool:
|
||||
"""Check if the point is at the origin (0, 0)"""
|
||||
return self == (0, 0)
|
||||
T = TypeVar("T")
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return self != (0, 0)
|
||||
|
||||
def __add__(self, other: object) -> Offset:
|
||||
if isinstance(other, tuple):
|
||||
_x, _y = self
|
||||
x, y = other
|
||||
return Offset(_x + x, _y + y)
|
||||
return NotImplemented
|
||||
def loop_first(values: Iterable[T]) -> Iterable[tuple[bool, T]]:
|
||||
"""Iterate and generate a tuple with a flag for first value."""
|
||||
iter_values = iter(values)
|
||||
try:
|
||||
value = next(iter_values)
|
||||
except StopIteration:
|
||||
return
|
||||
yield True, value
|
||||
for value in iter_values:
|
||||
yield False, value
|
||||
|
||||
def __sub__(self, other: object) -> Offset:
|
||||
if isinstance(other, tuple):
|
||||
_x, _y = self
|
||||
x, y = other
|
||||
return Offset(_x - x, _y - y)
|
||||
return NotImplemented
|
||||
|
||||
def __mul__(self, other: object) -> Offset:
|
||||
if isinstance(other, (float, int)):
|
||||
x, y = self
|
||||
return Offset(int(x * other), int(y * other))
|
||||
return NotImplemented
|
||||
def loop_last(values: Iterable[T]) -> Iterable[tuple[bool, T]]:
|
||||
"""Iterate and generate a tuple with a flag for last value."""
|
||||
iter_values = iter(values)
|
||||
try:
|
||||
previous_value = next(iter_values)
|
||||
except StopIteration:
|
||||
return
|
||||
for value in iter_values:
|
||||
yield False, previous_value
|
||||
previous_value = value
|
||||
yield True, previous_value
|
||||
|
||||
|
||||
def loop_first_last(values: Iterable[T]) -> Iterable[tuple[bool, bool, T]]:
|
||||
"""Iterate and generate a tuple with a flag for first and last value."""
|
||||
iter_values = iter(values)
|
||||
try:
|
||||
previous_value = next(iter_values)
|
||||
except StopIteration:
|
||||
return
|
||||
first = True
|
||||
for value in iter_values:
|
||||
yield first, False, previous_value
|
||||
first = False
|
||||
previous_value = value
|
||||
yield first, True, previous_value
|
||||
'''
|
||||
|
||||
|
||||
@@ -96,25 +109,28 @@ class BasicApp(App, css_path="basic.css"):
|
||||
|
||||
def on_load(self):
|
||||
"""Bind keys here."""
|
||||
self.bind("s", "toggle_class('#sidebar', '-active')")
|
||||
self.bind("s", "toggle_class('#sidebar', '-active')", description="Sidebar")
|
||||
self.bind("d", "toggle_dark", description="Dark mode")
|
||||
self.bind("q", "quit", description="Quit")
|
||||
self.bind("f", "query_test", description="Query test")
|
||||
|
||||
def compose(self):
|
||||
yield Header()
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
table = DataTable()
|
||||
self.scroll_to_target = Tweet(TweetBody())
|
||||
|
||||
yield Static(
|
||||
Text.from_markup(
|
||||
"[b]This is a [u]Textual[/u] app, running in the terminal"
|
||||
),
|
||||
id="header",
|
||||
)
|
||||
yield from (
|
||||
yield Container(
|
||||
Tweet(TweetBody()),
|
||||
Widget(
|
||||
Static(Syntax(CODE, "python"), classes="code"),
|
||||
Static(
|
||||
Syntax(CODE, "python", line_numbers=True, indent_guides=True),
|
||||
classes="code",
|
||||
),
|
||||
classes="scrollable",
|
||||
),
|
||||
table,
|
||||
Widget(DirectoryTree("~/"), id="tree-container"),
|
||||
Error(),
|
||||
Tweet(TweetBody(), classes="scrollbar-size-custom"),
|
||||
Warning(),
|
||||
@@ -126,7 +142,6 @@ class BasicApp(App, css_path="basic.css"):
|
||||
Tweet(TweetBody(), classes="scroll-horizontal"),
|
||||
Tweet(TweetBody(), classes="scroll-horizontal"),
|
||||
)
|
||||
yield Widget(id="footer")
|
||||
yield Widget(
|
||||
Widget(classes="title"),
|
||||
Widget(classes="user"),
|
||||
@@ -136,6 +151,7 @@ class BasicApp(App, css_path="basic.css"):
|
||||
Widget(classes="content"),
|
||||
id="sidebar",
|
||||
)
|
||||
yield Footer()
|
||||
|
||||
table.add_column("Foo", width=20)
|
||||
table.add_column("Bar", width=20)
|
||||
@@ -147,12 +163,32 @@ class BasicApp(App, css_path="basic.css"):
|
||||
for n in range(100):
|
||||
table.add_row(*[f"Cell ([b]{n}[/b], {col})" for col in range(6)])
|
||||
|
||||
def on_mount(self):
|
||||
self.sub_title = "Widget demo"
|
||||
|
||||
async def on_key(self, event) -> None:
|
||||
await self.dispatch_key(event)
|
||||
|
||||
def key_d(self):
|
||||
def action_toggle_dark(self):
|
||||
self.dark = not self.dark
|
||||
|
||||
def action_query_test(self):
|
||||
query = self.query("Tweet")
|
||||
self.log(query)
|
||||
self.log(query.nodes)
|
||||
self.log(query)
|
||||
self.log(query.nodes)
|
||||
|
||||
query.set_styles("outline: outer red;")
|
||||
|
||||
query = query.exclude(".scroll-horizontal")
|
||||
self.log(query)
|
||||
self.log(query.nodes)
|
||||
|
||||
# query = query.filter(".rubbish")
|
||||
# self.log(query)
|
||||
# self.log(query.first())
|
||||
|
||||
async def key_q(self):
|
||||
await self.shutdown()
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ Button {
|
||||
padding: 0 1;
|
||||
height: 100%;
|
||||
background: $primary-lighten-2;
|
||||
color: $text-primary-lighten-2;
|
||||
color: $text;
|
||||
}
|
||||
|
||||
#number-0 {
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
Screen {
|
||||
background: $surface-darken-1;
|
||||
}
|
||||
|
||||
#tree-view {
|
||||
display: none;
|
||||
scrollbar-gutter: stable;
|
||||
@@ -13,12 +17,12 @@ CodeBrowser.-show-tree #tree-view {
|
||||
}
|
||||
|
||||
CodeBrowser{
|
||||
background: $surface-darken-1;
|
||||
background: $background;
|
||||
}
|
||||
|
||||
DirectoryTree {
|
||||
padding-right: 1;
|
||||
|
||||
padding-right: 1;
|
||||
}
|
||||
|
||||
#code {
|
||||
|
||||
@@ -5,7 +5,7 @@ from rich.traceback import Traceback
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.layout import Container, Vertical
|
||||
from textual.reactive import Reactive
|
||||
from textual.reactive import var
|
||||
from textual.widgets import DirectoryTree, Footer, Header, Static
|
||||
|
||||
|
||||
@@ -13,11 +13,11 @@ class CodeBrowser(App):
|
||||
"""Textual code browser app."""
|
||||
|
||||
BINDINGS = [
|
||||
("t", "toggle_tree", "Toggle Tree"),
|
||||
("f", "toggle_files", "Toggle Files"),
|
||||
("q", "quit", "Quit"),
|
||||
]
|
||||
|
||||
show_tree = Reactive.init(True)
|
||||
show_tree = var(True)
|
||||
|
||||
def watch_show_tree(self, show_tree: bool) -> None:
|
||||
"""Called when show_tree is modified."""
|
||||
@@ -42,17 +42,17 @@ class CodeBrowser(App):
|
||||
line_numbers=True,
|
||||
word_wrap=True,
|
||||
indent_guides=True,
|
||||
theme="monokai",
|
||||
theme="github-dark",
|
||||
)
|
||||
except Exception:
|
||||
code_view.update(Traceback(theme="monokai", width=None))
|
||||
code_view.update(Traceback(theme="github-dark", width=None))
|
||||
self.sub_title = "ERROR"
|
||||
else:
|
||||
code_view.update(syntax)
|
||||
self.query_one("#code-view").scroll_home(animate=False)
|
||||
self.sub_title = event.path
|
||||
|
||||
def action_toggle_tree(self) -> None:
|
||||
def action_toggle_files(self) -> None:
|
||||
self.show_tree = not self.show_tree
|
||||
|
||||
|
||||
|
||||
172
imgs/calculator.svg
Normal file
|
After Width: | Height: | Size: 38 KiB |
193
imgs/codebrowser.svg
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
imgs/custom.gif
|
Before Width: | Height: | Size: 173 KiB |
BIN
imgs/custom.png
|
Before Width: | Height: | Size: 248 KiB |
199
imgs/stopwatch_dark.svg
Normal file
|
After Width: | Height: | Size: 36 KiB |
199
imgs/stopwatch_light.svg
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
imgs/textual.png
|
Before Width: | Height: | Size: 433 KiB After Width: | Height: | Size: 520 KiB |
BIN
imgs/widgets.png
|
Before Width: | Height: | Size: 234 KiB |
@@ -15,10 +15,10 @@
|
||||
App > Screen {
|
||||
|
||||
background: $surface;
|
||||
color: $text-surface;
|
||||
color: $text;
|
||||
layers: base sidebar;
|
||||
|
||||
color: $text-background;
|
||||
color: $text;
|
||||
background: $background;
|
||||
layout: vertical;
|
||||
|
||||
@@ -53,7 +53,7 @@ DataTable {
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
color: $text-panel;
|
||||
color: $text;
|
||||
background: $panel;
|
||||
dock: left;
|
||||
width: 30;
|
||||
@@ -70,7 +70,7 @@ DataTable {
|
||||
#sidebar .title {
|
||||
height: 1;
|
||||
background: $primary-background-darken-1;
|
||||
color: $text-primary-background-darken-1;
|
||||
color: $text-muted;
|
||||
border-right: wide $background;
|
||||
content-align: center middle;
|
||||
}
|
||||
@@ -78,14 +78,14 @@ DataTable {
|
||||
#sidebar .user {
|
||||
height: 8;
|
||||
background: $panel-darken-1;
|
||||
color: $text-panel-darken-1;
|
||||
color: $text-muted;
|
||||
border-right: wide $background;
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
#sidebar .content {
|
||||
background: $panel-darken-2;
|
||||
color: $text-surface;
|
||||
color: $text;
|
||||
border-right: wide $background;
|
||||
content-align: center middle;
|
||||
}
|
||||
@@ -99,7 +99,7 @@ Tweet {
|
||||
|
||||
|
||||
background: $panel;
|
||||
color: $text-panel;
|
||||
color: $text;
|
||||
layout: vertical;
|
||||
/* border: outer $primary; */
|
||||
padding: 1;
|
||||
@@ -129,13 +129,13 @@ Tweet {
|
||||
TweetHeader {
|
||||
height:1;
|
||||
background: $accent;
|
||||
color: $text-accent
|
||||
color: $text
|
||||
}
|
||||
|
||||
TweetBody {
|
||||
width: 100%;
|
||||
background: $panel;
|
||||
color: $text-panel;
|
||||
color: $text;
|
||||
height: auto;
|
||||
padding: 0 1 0 0;
|
||||
}
|
||||
@@ -146,7 +146,7 @@ Tweet.scroll-horizontal TweetBody {
|
||||
|
||||
.button {
|
||||
background: $accent;
|
||||
color: $text-accent;
|
||||
color: $text;
|
||||
width:20;
|
||||
height: 3;
|
||||
/* border-top: hidden $accent-darken-3; */
|
||||
@@ -162,7 +162,7 @@ Tweet.scroll-horizontal TweetBody {
|
||||
|
||||
.button:hover {
|
||||
background: $accent-lighten-1;
|
||||
color: $text-accent-lighten-1;
|
||||
color: $text-disabled;
|
||||
width: 20;
|
||||
height: 3;
|
||||
border: tall $accent-darken-1;
|
||||
@@ -174,7 +174,7 @@ Tweet.scroll-horizontal TweetBody {
|
||||
}
|
||||
|
||||
#footer {
|
||||
color: $text-accent;
|
||||
color: $text;
|
||||
background: $accent;
|
||||
height: 1;
|
||||
|
||||
@@ -197,7 +197,7 @@ OptionItem {
|
||||
|
||||
OptionItem:hover {
|
||||
height: 3;
|
||||
color: $text-primary;
|
||||
color: $text;
|
||||
background: $primary-darken-1;
|
||||
/* border-top: hkey $accent2-darken-3;
|
||||
border-bottom: hkey $accent2-darken-3; */
|
||||
@@ -209,7 +209,7 @@ Error {
|
||||
width: 100%;
|
||||
height:3;
|
||||
background: $error;
|
||||
color: $text-error;
|
||||
color: $text;
|
||||
border-top: tall $error-darken-2;
|
||||
border-bottom: tall $error-darken-2;
|
||||
|
||||
@@ -222,7 +222,7 @@ Warning {
|
||||
width: 100%;
|
||||
height:3;
|
||||
background: $warning;
|
||||
color: $text-warning-fade-1;
|
||||
color: $text-muted;
|
||||
border-top: tall $warning-darken-2;
|
||||
border-bottom: tall $warning-darken-2;
|
||||
|
||||
@@ -236,7 +236,7 @@ Success {
|
||||
height:auto;
|
||||
box-sizing: border-box;
|
||||
background: $success;
|
||||
color: $text-success-fade-1;
|
||||
color: $text-muted;
|
||||
|
||||
border-top: hkey $success-darken-2;
|
||||
border-bottom: hkey $success-darken-2;
|
||||
|
||||
@@ -14,16 +14,11 @@
|
||||
|
||||
App > Screen {
|
||||
|
||||
background: $surface;
|
||||
color: $text-surface;
|
||||
layers: base sidebar;
|
||||
|
||||
color: $text-background;
|
||||
background: $background;
|
||||
color: $text;
|
||||
layers: base sidebar;
|
||||
layout: vertical;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
}
|
||||
|
||||
#tree-container {
|
||||
@@ -53,8 +48,8 @@ DataTable {
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
color: $text-panel;
|
||||
background: $panel;
|
||||
color: $text;
|
||||
dock: left;
|
||||
width: 30;
|
||||
margin-bottom: 1;
|
||||
@@ -71,7 +66,7 @@ DataTable {
|
||||
#sidebar .title {
|
||||
height: 1;
|
||||
background: $primary-background-darken-1;
|
||||
color: $text-primary-background-darken-1;
|
||||
color: $text;
|
||||
border-right: wide $background;
|
||||
content-align: center middle;
|
||||
}
|
||||
@@ -79,14 +74,14 @@ DataTable {
|
||||
#sidebar .user {
|
||||
height: 8;
|
||||
background: $panel-darken-1;
|
||||
color: $text-panel-darken-1;
|
||||
color: $text;
|
||||
border-right: wide $background;
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
#sidebar .content {
|
||||
background: $panel-darken-2;
|
||||
color: $text-surface;
|
||||
color: $text;
|
||||
border-right: wide $background;
|
||||
content-align: center middle;
|
||||
}
|
||||
@@ -101,7 +96,7 @@ Tweet {
|
||||
|
||||
margin:0 2;
|
||||
background: $panel;
|
||||
color: $text-panel;
|
||||
color: $text;
|
||||
layout: vertical;
|
||||
/* border: outer $primary; */
|
||||
padding: 1;
|
||||
@@ -132,13 +127,13 @@ Tweet {
|
||||
TweetHeader {
|
||||
height:1;
|
||||
background: $accent;
|
||||
color: $text-accent
|
||||
color: $text;
|
||||
}
|
||||
|
||||
TweetBody {
|
||||
width: 100%;
|
||||
background: $panel;
|
||||
color: $text-panel;
|
||||
color: $text;
|
||||
height: auto;
|
||||
padding: 0 1 0 0;
|
||||
}
|
||||
@@ -149,7 +144,7 @@ Tweet.scroll-horizontal TweetBody {
|
||||
|
||||
.button {
|
||||
background: $accent;
|
||||
color: $text-accent;
|
||||
color: $text;
|
||||
width:20;
|
||||
height: 3;
|
||||
/* border-top: hidden $accent-darken-3; */
|
||||
@@ -165,7 +160,7 @@ Tweet.scroll-horizontal TweetBody {
|
||||
|
||||
.button:hover {
|
||||
background: $accent-lighten-1;
|
||||
color: $text-accent-lighten-1;
|
||||
color: $text;
|
||||
width: 20;
|
||||
height: 3;
|
||||
border: tall $accent-darken-1;
|
||||
@@ -177,7 +172,7 @@ Tweet.scroll-horizontal TweetBody {
|
||||
}
|
||||
|
||||
#footer {
|
||||
color: $text-accent;
|
||||
color: $text;
|
||||
background: $accent;
|
||||
height: 1;
|
||||
|
||||
@@ -200,7 +195,7 @@ OptionItem {
|
||||
|
||||
OptionItem:hover {
|
||||
height: 3;
|
||||
color: $text-primary;
|
||||
color: $text;
|
||||
background: $primary-darken-1;
|
||||
/* border-top: hkey $accent2-darken-3;
|
||||
border-bottom: hkey $accent2-darken-3; */
|
||||
@@ -212,7 +207,7 @@ Error {
|
||||
width: 100%;
|
||||
height:3;
|
||||
background: $error;
|
||||
color: $text-error;
|
||||
color: $text;
|
||||
border-top: tall $error-darken-2;
|
||||
border-bottom: tall $error-darken-2;
|
||||
|
||||
@@ -225,7 +220,7 @@ Warning {
|
||||
width: 100%;
|
||||
height:3;
|
||||
background: $warning;
|
||||
color: $text-warning-fade-1;
|
||||
color: $text;
|
||||
border-top: tall $warning-darken-2;
|
||||
border-bottom: tall $warning-darken-2;
|
||||
|
||||
@@ -239,7 +234,7 @@ Success {
|
||||
height:auto;
|
||||
box-sizing: border-box;
|
||||
background: $success;
|
||||
color: $text-success-fade-1;
|
||||
color: $text;
|
||||
|
||||
border-top: hkey $success-darken-2;
|
||||
border-bottom: hkey $success-darken-2;
|
||||
|
||||
@@ -24,7 +24,7 @@ Button {
|
||||
padding: 0 1;
|
||||
height: 100%;
|
||||
background: $primary-lighten-2;
|
||||
color: $text-primary-lighten-2;
|
||||
color: $text;
|
||||
}
|
||||
|
||||
#number-0 {
|
||||
|
||||
@@ -33,7 +33,7 @@ class CenterApp(App):
|
||||
|
||||
Static {
|
||||
background: $panel;
|
||||
color: $text-panel;
|
||||
color: $text;
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
|
||||
23
sandbox/will/design.css
Normal file
@@ -0,0 +1,23 @@
|
||||
Screen {
|
||||
background: $surface;
|
||||
}
|
||||
|
||||
Container {
|
||||
height: auto;
|
||||
background: $boost;
|
||||
|
||||
}
|
||||
|
||||
Panel {
|
||||
height: auto;
|
||||
background: $boost;
|
||||
margin: 1 2;
|
||||
|
||||
}
|
||||
|
||||
Content {
|
||||
background: $boost;
|
||||
padding: 1 2;
|
||||
margin: 1 2;
|
||||
color: auto 95%;
|
||||
}
|
||||
35
sandbox/will/design.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from textual.app import App
|
||||
from textual.layout import Container
|
||||
from textual.widgets import Header, Footer, Static
|
||||
|
||||
|
||||
class Content(Static):
|
||||
pass
|
||||
|
||||
|
||||
class Panel(Container):
|
||||
pass
|
||||
|
||||
|
||||
class Panel2(Container):
|
||||
pass
|
||||
|
||||
|
||||
class DesignApp(App):
|
||||
BINDINGS = [("d", "toggle_dark", "Toggle dark mode")]
|
||||
|
||||
def compose(self):
|
||||
yield Header()
|
||||
yield Footer()
|
||||
yield Container(
|
||||
Content("content"),
|
||||
Panel(
|
||||
Content("more content"),
|
||||
Content("more content"),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
app = DesignApp(css_path="design.css")
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
||||
21
sandbox/will/offset.css
Normal file
@@ -0,0 +1,21 @@
|
||||
Screen {
|
||||
layout: center;
|
||||
}
|
||||
|
||||
#parent {
|
||||
width: 32;
|
||||
height: 8;
|
||||
background: $panel;
|
||||
}
|
||||
|
||||
#tag {
|
||||
color: $text;
|
||||
background: $success;
|
||||
padding: 2 4;
|
||||
width: auto;
|
||||
offset: -8 -4;
|
||||
}
|
||||
|
||||
#child {
|
||||
background: red;
|
||||
}
|
||||
17
sandbox/will/offset.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from textual import layout
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.widgets import Static
|
||||
|
||||
|
||||
class OffsetExample(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield layout.Vertical(
|
||||
Static("Child", id="child"),
|
||||
id="parent"
|
||||
)
|
||||
yield Static("Tag", id="tag")
|
||||
|
||||
|
||||
app = OffsetExample(css_path="offset.css")
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
||||
@@ -39,8 +39,8 @@ BORDER_CHARS: dict[EdgeType, tuple[str, str, str]] = {
|
||||
"outer": ("▛▀▜", "▌ ▐", "▙▄▟"),
|
||||
"hkey": ("▔▔▔", " ", "▁▁▁"),
|
||||
"vkey": ("▏ ▕", "▏ ▕", "▏ ▕"),
|
||||
"tall": ("▕▔▏", "▕ ▏", "▕▁▏"),
|
||||
"wide": ("▁▁▁", "▏ ▕", "▔▔▔"),
|
||||
"tall": ("▊▔▎", "▊ ▎", "▊▁▎"),
|
||||
"wide": ("▁▁▁", "▎ ▋", "▔▔▔"),
|
||||
}
|
||||
|
||||
# Some of the borders are on the widget background and some are on the background of the parent
|
||||
@@ -62,8 +62,8 @@ BORDER_LOCATIONS: dict[
|
||||
"outer": ((0, 0, 0), (0, 0, 0), (0, 0, 0)),
|
||||
"hkey": ((0, 0, 0), (0, 0, 0), (0, 0, 0)),
|
||||
"vkey": ((0, 0, 0), (0, 0, 0), (0, 0, 0)),
|
||||
"tall": ((1, 0, 1), (1, 0, 1), (1, 0, 1)),
|
||||
"wide": ((1, 1, 1), (0, 1, 0), (1, 1, 1)),
|
||||
"tall": ((2, 0, 1), (2, 0, 1), (2, 0, 1)),
|
||||
"wide": ((1, 1, 1), (0, 1, 3), (1, 1, 1)),
|
||||
}
|
||||
|
||||
INVISIBLE_EDGE_TYPES = cast("frozenset[EdgeType]", frozenset(("", "none", "hidden")))
|
||||
@@ -81,7 +81,10 @@ Borders: TypeAlias = Tuple[EdgeStyle, EdgeStyle, EdgeStyle, EdgeStyle]
|
||||
|
||||
@lru_cache(maxsize=1024)
|
||||
def get_box(
|
||||
name: EdgeType, inner_style: Style, outer_style: Style, style: Style
|
||||
name: EdgeType,
|
||||
inner_style: Style,
|
||||
outer_style: Style,
|
||||
style: Style,
|
||||
) -> BoxSegments:
|
||||
"""Get segments used to render a box.
|
||||
|
||||
@@ -107,23 +110,30 @@ def get_box(
|
||||
(lbottom1, lbottom2, lbottom3),
|
||||
) = BORDER_LOCATIONS[name]
|
||||
|
||||
styles = (inner_style, outer_style)
|
||||
inner = inner_style + style
|
||||
outer = outer_style + style
|
||||
styles = (
|
||||
inner,
|
||||
outer,
|
||||
Style.from_color(outer.bgcolor, inner.color),
|
||||
Style.from_color(inner.bgcolor, outer.color),
|
||||
)
|
||||
|
||||
return (
|
||||
(
|
||||
_Segment(top1, styles[ltop1] + style),
|
||||
_Segment(top2, styles[ltop2] + style),
|
||||
_Segment(top3, styles[ltop3] + style),
|
||||
_Segment(top1, styles[ltop1]),
|
||||
_Segment(top2, styles[ltop2]),
|
||||
_Segment(top3, styles[ltop3]),
|
||||
),
|
||||
(
|
||||
_Segment(mid1, styles[lmid1] + style),
|
||||
_Segment(mid2, styles[lmid2] + style),
|
||||
_Segment(mid3, styles[lmid3] + style),
|
||||
_Segment(mid1, styles[lmid1]),
|
||||
_Segment(mid2, styles[lmid2]),
|
||||
_Segment(mid3, styles[lmid3]),
|
||||
),
|
||||
(
|
||||
_Segment(bottom1, styles[lbottom1] + style),
|
||||
_Segment(bottom2, styles[lbottom2] + style),
|
||||
_Segment(bottom3, styles[lbottom3] + style),
|
||||
_Segment(bottom1, styles[lbottom1]),
|
||||
_Segment(bottom2, styles[lbottom2]),
|
||||
_Segment(bottom3, styles[lbottom3]),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ without having to render the entire screen.
|
||||
from __future__ import annotations
|
||||
|
||||
from itertools import chain
|
||||
from operator import attrgetter, itemgetter
|
||||
from operator import itemgetter
|
||||
import sys
|
||||
from typing import Callable, cast, Iterator, Iterable, NamedTuple, TYPE_CHECKING
|
||||
|
||||
@@ -26,7 +26,7 @@ from rich.segment import Segment
|
||||
from rich.style import Style
|
||||
|
||||
from . import errors
|
||||
from .geometry import Region, Offset, Size, Spacing
|
||||
from .geometry import Region, Offset, Size
|
||||
|
||||
from ._cells import cell_len
|
||||
from ._profile import timer
|
||||
@@ -55,7 +55,7 @@ class MapGeometry(NamedTuple):
|
||||
"""Defines the absolute location of a Widget."""
|
||||
|
||||
region: Region # The (screen) region occupied by the widget
|
||||
order: tuple[int, ...] # A tuple of ints defining the painting order
|
||||
order: tuple[tuple[int, ...], ...] # A tuple of ints defining the painting order
|
||||
clip: Region # A region to clip the widget by (if a Widget is within a container)
|
||||
virtual_size: Size # The virtual size (scrollable region) of a widget if it is a container
|
||||
container_size: Size # The container size (area not occupied by scrollbars)
|
||||
@@ -344,12 +344,14 @@ class Compositor:
|
||||
|
||||
map: CompositorMap = {}
|
||||
widgets: set[Widget] = set()
|
||||
layer_order: int = 0
|
||||
|
||||
def add_widget(
|
||||
widget: Widget,
|
||||
virtual_region: Region,
|
||||
region: Region,
|
||||
order: tuple[int, ...],
|
||||
order: tuple[tuple[int, ...], ...],
|
||||
layer_order: int,
|
||||
clip: Region,
|
||||
) -> None:
|
||||
"""Called recursively to place a widget and its children in the map.
|
||||
@@ -413,15 +415,19 @@ class Compositor:
|
||||
)
|
||||
widget_region = sub_region + placement_scroll_offset
|
||||
|
||||
widget_order = order + (get_layer_index(sub_widget.layer, 0), z)
|
||||
widget_order = order + (
|
||||
(get_layer_index(sub_widget.layer, 0), z, layer_order),
|
||||
)
|
||||
|
||||
add_widget(
|
||||
sub_widget,
|
||||
sub_region,
|
||||
widget_region,
|
||||
widget_order,
|
||||
layer_order,
|
||||
sub_clip,
|
||||
)
|
||||
layer_order -= 1
|
||||
|
||||
# Add any scrollbars
|
||||
for chrome_widget, chrome_region in widget._arrange_scrollbars(
|
||||
@@ -457,7 +463,7 @@ class Compositor:
|
||||
)
|
||||
|
||||
# Add top level (root) widget
|
||||
add_widget(root, size.region, size.region, (0,), size.region)
|
||||
add_widget(root, size.region, size.region, ((0,),), layer_order, size.region)
|
||||
return map, widgets
|
||||
|
||||
@property
|
||||
|
||||
@@ -250,7 +250,7 @@ class StylesCache:
|
||||
line: Iterable[Segment]
|
||||
# Draw top or bottom borders (A)
|
||||
if (border_top and y == 0) or (border_bottom and y == height - 1):
|
||||
border_color = background + (
|
||||
border_color = base_background + (
|
||||
border_top_color if y == 0 else border_bottom_color
|
||||
)
|
||||
box_segments = get_box(
|
||||
@@ -271,9 +271,9 @@ class StylesCache:
|
||||
pad_bottom and y >= height - gutter.bottom
|
||||
):
|
||||
background_style = from_color(bgcolor=background.rich_color)
|
||||
left_style = from_color(color=border_left_color.rich_color)
|
||||
left_style = from_color(color=(background + border_left_color).rich_color)
|
||||
left = get_box(border_left, inner, outer, left_style)[1][0]
|
||||
right_style = from_color(color=border_right_color.rich_color)
|
||||
right_style = from_color(color=(background + border_right_color).rich_color)
|
||||
right = get_box(border_right, inner, outer, right_style)[1][2]
|
||||
if border_left and border_right:
|
||||
line = [left, Segment(" " * (width - 2), background_style), right]
|
||||
@@ -296,9 +296,13 @@ class StylesCache:
|
||||
|
||||
if border_left or border_right:
|
||||
# Add left / right border
|
||||
left_style = from_color((background + border_left_color).rich_color)
|
||||
left_style = from_color(
|
||||
(base_background + border_left_color).rich_color
|
||||
)
|
||||
left = get_box(border_left, inner, outer, left_style)[1][0]
|
||||
right_style = from_color((background + border_right_color).rich_color)
|
||||
right_style = from_color(
|
||||
(base_background + border_right_color).rich_color
|
||||
)
|
||||
right = get_box(border_right, inner, outer, right_style)[1][2]
|
||||
|
||||
if border_left and border_right:
|
||||
@@ -327,9 +331,9 @@ class StylesCache:
|
||||
|
||||
elif outline_left or outline_right:
|
||||
# Lines in side outline
|
||||
left_style = from_color((background + outline_left_color).rich_color)
|
||||
left_style = from_color((base_background + outline_left_color).rich_color)
|
||||
left = get_box(outline_left, inner, outer, left_style)[1][0]
|
||||
right_style = from_color((background + outline_right_color).rich_color)
|
||||
right_style = from_color((base_background + outline_right_color).rich_color)
|
||||
right = get_box(outline_right, inner, outer, right_style)[1][2]
|
||||
line = line_trim(list(line), outline_left != "", outline_right != "")
|
||||
if outline_left and outline_right:
|
||||
|
||||
@@ -28,7 +28,7 @@ import rich.repr
|
||||
from rich.console import Console, RenderableType
|
||||
from rich.measure import Measurement
|
||||
from rich.protocol import is_renderable
|
||||
from rich.segment import Segments
|
||||
from rich.segment import Segment, Segments
|
||||
from rich.traceback import Traceback
|
||||
|
||||
from . import (
|
||||
@@ -146,7 +146,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
DEFAULT_CSS = """
|
||||
App {
|
||||
background: $background;
|
||||
color: $text-background;
|
||||
color: $text;
|
||||
}
|
||||
"""
|
||||
|
||||
@@ -541,9 +541,14 @@ class App(Generic[ReturnType], DOMNode):
|
||||
except Exception as error:
|
||||
self._handle_exception(error)
|
||||
|
||||
def action_screenshot(self, path: str | None = None) -> None:
|
||||
def action_toggle_dark(self) -> None:
|
||||
"""Action to toggle dark mode."""
|
||||
self.dark = not self.dark
|
||||
|
||||
def action_screenshot(self, filename: str | None, path: str = "~/") -> None:
|
||||
"""Action to save a screenshot."""
|
||||
self.save_screenshot(path)
|
||||
self.bell()
|
||||
self.save_screenshot(filename, path)
|
||||
|
||||
def export_screenshot(self, *, title: str | None = None) -> str:
|
||||
"""Export a SVG screenshot of the current screen.
|
||||
@@ -566,22 +571,31 @@ class App(Generic[ReturnType], DOMNode):
|
||||
console.print(screen_render)
|
||||
return console.export_svg(title=title or self.title)
|
||||
|
||||
def save_screenshot(self, path: str | None = None) -> str:
|
||||
"""Save a screenshot of the current screen.
|
||||
def save_screenshot(
|
||||
self,
|
||||
filename: str | None = None,
|
||||
path: str = "./",
|
||||
time_format: str = "%Y-%m-%d %X %f",
|
||||
) -> str:
|
||||
"""Save a SVG screenshot of the current screen.
|
||||
|
||||
Args:
|
||||
path (str | None, optional): Path to SVG to save or None to pick
|
||||
a filename automatically. Defaults to None.
|
||||
filename (str | None, optional): Filename of SVG screenshot, or None to auto-generate
|
||||
a filename with the date and time. Defaults to None.
|
||||
path (str, optional): Path to directory for output. Defaults to current working directory.
|
||||
time_format(str, optional): Time format to use if filename is None. Defaults to "%Y-%m-%d %X %f".
|
||||
|
||||
Returns:
|
||||
str: Filename of screenshot.
|
||||
"""
|
||||
self.bell()
|
||||
if path is None:
|
||||
svg_path = f"{self.title.lower()}_{datetime.now().isoformat()}.svg"
|
||||
svg_path = svg_path.replace("/", "_").replace("\\", "_")
|
||||
if filename is None:
|
||||
svg_filename = (
|
||||
f"{self.title.lower()} {datetime.now().strftime(time_format)}.svg"
|
||||
)
|
||||
svg_filename = svg_filename.replace("/", "_").replace("\\", "_")
|
||||
else:
|
||||
svg_path = path
|
||||
svg_filename = filename
|
||||
svg_path = os.path.expanduser(os.path.join(path, svg_filename))
|
||||
screenshot_svg = self.export_screenshot()
|
||||
with open(svg_path, "w") as svg_file:
|
||||
svg_file.write(screenshot_svg)
|
||||
@@ -1002,11 +1016,12 @@ class App(Generic[ReturnType], DOMNode):
|
||||
is_renderable(renderable) for renderable in renderables
|
||||
), "Can only call panic with strings or Rich renderables"
|
||||
|
||||
pre_rendered = [
|
||||
Segments(self.console.render(renderable, self.console.options))
|
||||
for renderable in renderables
|
||||
]
|
||||
def render(renderable: RenderableType) -> list[Segment]:
|
||||
"""Render a panic renderables."""
|
||||
segments = list(self.console.render(renderable, self.console.options))
|
||||
return segments
|
||||
|
||||
pre_rendered = [Segments(render(renderable)) for renderable in renderables]
|
||||
self._exit_renderables.extend(pre_rendered)
|
||||
self._close_messages_no_wait()
|
||||
|
||||
|
||||
@@ -38,10 +38,10 @@ class BorderApp(App):
|
||||
Static {
|
||||
margin: 2 4;
|
||||
padding: 2 4;
|
||||
border: solid $primary;
|
||||
border: solid $secondary;
|
||||
height: auto;
|
||||
background: $panel;
|
||||
color: $text-panel;
|
||||
color: $text;
|
||||
}
|
||||
"""
|
||||
|
||||
@@ -53,7 +53,7 @@ class BorderApp(App):
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
self.text.styles.border = (
|
||||
event.button.id,
|
||||
self.stylesheet.variables["primary"],
|
||||
self.stylesheet._variables["secondary"],
|
||||
)
|
||||
self.bell()
|
||||
|
||||
|
||||
@@ -13,12 +13,20 @@ EasingButtons {
|
||||
|
||||
#duration-input {
|
||||
width: 30;
|
||||
background: $boost;
|
||||
padding: 0 1;
|
||||
border: tall transparent;
|
||||
}
|
||||
|
||||
#duration-input:focus {
|
||||
border: tall $accent;
|
||||
}
|
||||
|
||||
#inputs {
|
||||
padding: 1;
|
||||
height: auto;
|
||||
dock: top;
|
||||
background: $boost;
|
||||
}
|
||||
|
||||
Bar {
|
||||
@@ -36,6 +44,11 @@ Bar {
|
||||
#opacity-widget {
|
||||
padding: 1;
|
||||
background: $warning;
|
||||
color: $text-warning;
|
||||
color: $text;
|
||||
border: wide $background;
|
||||
}
|
||||
|
||||
#label {
|
||||
width: auto;
|
||||
padding: 1;
|
||||
}
|
||||
|
||||
@@ -79,7 +79,9 @@ class EasingApp(App):
|
||||
|
||||
yield EasingButtons()
|
||||
yield layout.Vertical(
|
||||
layout.Vertical(Static("Animation Duration:"), duration_input, id="inputs"),
|
||||
layout.Horizontal(
|
||||
Static("Animation Duration:", id="label"), duration_input, id="inputs"
|
||||
),
|
||||
layout.Horizontal(
|
||||
self.animated_bar,
|
||||
layout.Container(self.opacity_widget, id="other"),
|
||||
|
||||
@@ -283,6 +283,17 @@ class Color(NamedTuple):
|
||||
else f"#{r:02X}{g:02X}{b:02X}{int(a*255):02X}"
|
||||
)
|
||||
|
||||
@property
|
||||
def hex6(self) -> str:
|
||||
"""The color in CSS hex form, with 6 digits for RGB. Alpha is ignored.
|
||||
|
||||
Returns:
|
||||
str: A CSS hex-style color, e.g. "#46b3de"
|
||||
|
||||
"""
|
||||
r, g, b, a = self.clamped
|
||||
return f"#{r:02X}{g:02X}{b:02X}"
|
||||
|
||||
@property
|
||||
def css(self) -> str:
|
||||
"""The color in CSS rgb or rgba form.
|
||||
@@ -313,12 +324,13 @@ class Color(NamedTuple):
|
||||
r, g, b, _ = self
|
||||
return Color(r, g, b, alpha)
|
||||
|
||||
def blend(self, destination: Color, factor: float) -> Color:
|
||||
def blend(self, destination: Color, factor: float, alpha: float = 1) -> Color:
|
||||
"""Generate a new color between two colors.
|
||||
|
||||
Args:
|
||||
destination (Color): Another color.
|
||||
factor (float): A blend factor, 0 -> 1
|
||||
factor (float): A blend factor, 0 -> 1.
|
||||
alpha (float | None): New alpha for result. Defaults to 1.
|
||||
|
||||
Returns:
|
||||
Color: A new color.
|
||||
@@ -333,6 +345,7 @@ class Color(NamedTuple):
|
||||
int(r1 + (r2 - r1) * factor),
|
||||
int(g1 + (g2 - g1) * factor),
|
||||
int(b1 + (b2 - b1) * factor),
|
||||
alpha,
|
||||
)
|
||||
|
||||
def __add__(self, other: object) -> Color:
|
||||
@@ -452,29 +465,31 @@ class Color(NamedTuple):
|
||||
return color
|
||||
|
||||
@lru_cache(maxsize=1024)
|
||||
def darken(self, amount: float) -> Color:
|
||||
def darken(self, amount: float, alpha: float | None = None) -> Color:
|
||||
"""Darken the color by a given amount.
|
||||
|
||||
Args:
|
||||
amount (float): Value between 0-1 to reduce luminance by.
|
||||
alpha (float | None, optional): Alpha component for new color or None to copy alpha. Defaults to None.
|
||||
|
||||
Returns:
|
||||
Color: New color.
|
||||
"""
|
||||
l, a, b = rgb_to_lab(self)
|
||||
l -= amount * 100
|
||||
return lab_to_rgb(Lab(l, a, b)).clamped
|
||||
return lab_to_rgb(Lab(l, a, b), self.a if alpha is None else alpha).clamped
|
||||
|
||||
def lighten(self, amount: float) -> Color:
|
||||
def lighten(self, amount: float, alpha: float | None = None) -> Color:
|
||||
"""Lighten the color by a given amount.
|
||||
|
||||
Args:
|
||||
amount (float): Value between 0-1 to increase luminance by.
|
||||
alpha (float | None, optional): Alpha component for new color or None to copy alpha. Defaults to None.
|
||||
|
||||
Returns:
|
||||
Color: New color.
|
||||
"""
|
||||
return self.darken(-amount)
|
||||
return self.darken(-amount, alpha)
|
||||
|
||||
@lru_cache(maxsize=1024)
|
||||
def get_contrast_text(self, alpha=0.95) -> Color:
|
||||
@@ -527,7 +542,7 @@ def rgb_to_lab(rgb: Color) -> Lab:
|
||||
return Lab(116 * y - 16, 500 * (x - y), 200 * (y - z))
|
||||
|
||||
|
||||
def lab_to_rgb(lab: Lab) -> Color:
|
||||
def lab_to_rgb(lab: Lab, alpha: float = 1.0) -> Color:
|
||||
"""Convert a CIE-L*ab color to RGB.
|
||||
|
||||
Uses the standard RGB color space with a D65/2⁰ standard illuminant.
|
||||
@@ -552,4 +567,4 @@ def lab_to_rgb(lab: Lab) -> Color:
|
||||
g = 1.055 * pow(g, 1 / 2.4) - 0.055 if g > 0.0031308 else 12.92 * g
|
||||
b = 1.055 * pow(b, 1 / 2.4) - 0.055 if b > 0.0031308 else 12.92 * b
|
||||
|
||||
return Color(int(r * 255), int(g * 255), int(b * 255))
|
||||
return Color(int(r * 255), int(g * 255), int(b * 255), alpha)
|
||||
|
||||
@@ -2,8 +2,8 @@ from __future__ import annotations
|
||||
|
||||
from typing import Iterable
|
||||
|
||||
import rich.repr
|
||||
from rich.console import Console, ConsoleOptions, RenderResult
|
||||
|
||||
from rich.highlighter import ReprHighlighter
|
||||
from rich.markup import render
|
||||
from rich.text import Text
|
||||
@@ -42,6 +42,7 @@ class Example:
|
||||
yield _markup_and_highlight(f" [dim]e.g. [/][i]{self.markup}[/]")
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
class Bullet:
|
||||
"""Renderable for a single 'bullet point' containing information and optionally some examples
|
||||
pertaining to that information.
|
||||
@@ -59,10 +60,11 @@ class Bullet:
|
||||
def __rich_console__(
|
||||
self, console: Console, options: ConsoleOptions
|
||||
) -> RenderResult:
|
||||
yield _markup_and_highlight(f"{self.markup}")
|
||||
yield _markup_and_highlight(self.markup)
|
||||
yield from self.examples
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
class HelpText:
|
||||
"""Renderable for help text - the user is shown this when they
|
||||
encounter a style-related error (e.g. setting a style property to an invalid
|
||||
|
||||
@@ -102,6 +102,13 @@ class IntegerProperty(GenericProperty[int, int]):
|
||||
raise StyleValueError(f"Expected a number here, got f{value}")
|
||||
|
||||
|
||||
class BooleanProperty(GenericProperty[bool, bool]):
|
||||
"""A property that requires a True or False value."""
|
||||
|
||||
def validate_value(self, value: object) -> bool:
|
||||
return bool(value)
|
||||
|
||||
|
||||
class ScalarProperty:
|
||||
"""Descriptor for getting and setting scalar properties. Scalars are numeric values with a unit, e.g. "50vh"."""
|
||||
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import lru_cache
|
||||
from typing import cast, Iterable, NoReturn, Sequence
|
||||
from typing import Iterable, NoReturn, Sequence, cast
|
||||
|
||||
import rich.repr
|
||||
|
||||
from .._border import BorderValue, normalize_border_value
|
||||
from .._duration import _duration_as_seconds
|
||||
from .._easing import EASING
|
||||
from ..color import Color, ColorParseError
|
||||
from ..geometry import Spacing, SpacingDimensions, clamp
|
||||
from ..suggestions import get_suggestion
|
||||
from ._error_tools import friendly_list
|
||||
from ._help_renderables import HelpText
|
||||
from ._help_text import (
|
||||
@@ -33,34 +39,28 @@ from .constants import (
|
||||
VALID_ALIGN_VERTICAL,
|
||||
VALID_BORDER,
|
||||
VALID_BOX_SIZING,
|
||||
VALID_EDGE,
|
||||
VALID_DISPLAY,
|
||||
VALID_EDGE,
|
||||
VALID_OVERFLOW,
|
||||
VALID_VISIBILITY,
|
||||
VALID_STYLE_FLAGS,
|
||||
VALID_SCROLLBAR_GUTTER,
|
||||
VALID_STYLE_FLAGS,
|
||||
VALID_TEXT_ALIGN,
|
||||
VALID_VISIBILITY,
|
||||
)
|
||||
from .errors import DeclarationError, StyleValueError
|
||||
from .model import Declaration
|
||||
from .scalar import (
|
||||
Scalar,
|
||||
ScalarOffset,
|
||||
Unit,
|
||||
ScalarError,
|
||||
ScalarOffset,
|
||||
ScalarParseError,
|
||||
Unit,
|
||||
percentage_string_to_float,
|
||||
)
|
||||
from .styles import DockGroup, Styles
|
||||
from .styles import Styles
|
||||
from .tokenize import Token
|
||||
from .transition import Transition
|
||||
from .types import BoxSizing, Edge, Display, Overflow, Visibility, EdgeType
|
||||
from .._border import normalize_border_value, BorderValue
|
||||
from ..color import Color, ColorParseError
|
||||
from .._duration import _duration_as_seconds
|
||||
from .._easing import EASING
|
||||
from ..geometry import Spacing, SpacingDimensions, clamp
|
||||
from ..suggestions import get_suggestion
|
||||
from .types import BoxSizing, Display, Edge, EdgeType, Overflow, Visibility
|
||||
|
||||
|
||||
def _join_tokens(tokens: Iterable[Token], joiner: str = "") -> str:
|
||||
@@ -434,6 +434,7 @@ class StylesBuilder:
|
||||
process_padding_left = _process_space_partial
|
||||
|
||||
def _parse_border(self, name: str, tokens: list[Token]) -> BorderValue:
|
||||
|
||||
border_type: EdgeType = "solid"
|
||||
border_color = Color(0, 255, 0)
|
||||
|
||||
@@ -553,7 +554,7 @@ class StylesBuilder:
|
||||
self.styles._rules["offset"] = ScalarOffset(x, y)
|
||||
|
||||
def process_layout(self, name: str, tokens: list[Token]) -> None:
|
||||
from ..layouts.factory import get_layout, MissingLayout
|
||||
from ..layouts.factory import MissingLayout, get_layout
|
||||
|
||||
if tokens:
|
||||
if len(tokens) != 1:
|
||||
@@ -580,7 +581,9 @@ class StylesBuilder:
|
||||
alpha: float | None = None
|
||||
|
||||
for token in tokens:
|
||||
if token.name == "scalar":
|
||||
if name == "color" and token.name == "token" and token.value == "auto":
|
||||
self.styles._rules["auto_color"] = True
|
||||
elif token.name == "scalar":
|
||||
alpha_scalar = Scalar.parse(token.value)
|
||||
if alpha_scalar.unit != Unit.PERCENT:
|
||||
self.error(name, token, "alpha must be given as a percentage.")
|
||||
@@ -598,9 +601,9 @@ class StylesBuilder:
|
||||
else:
|
||||
self.error(name, token, color_property_help_text(name, context="css"))
|
||||
|
||||
if color is not None:
|
||||
if color is not None or alpha is not None:
|
||||
if alpha is not None:
|
||||
color = color.with_alpha(alpha)
|
||||
color = (color or Color(255, 255, 255)).with_alpha(alpha)
|
||||
self.styles._rules[name] = color
|
||||
|
||||
process_tint = process_color
|
||||
|
||||
@@ -301,9 +301,7 @@ def substitute_references(
|
||||
for _token in reference_tokens:
|
||||
yield _token.with_reference(
|
||||
ReferencedBy(
|
||||
name=ref_name,
|
||||
location=ref_location,
|
||||
length=ref_length,
|
||||
ref_name, ref_location, ref_length, token.code
|
||||
)
|
||||
)
|
||||
else:
|
||||
@@ -318,13 +316,10 @@ def substitute_references(
|
||||
variable_tokens = variables[variable_name]
|
||||
ref_location = token.location
|
||||
ref_length = len(token.value)
|
||||
for token in variable_tokens:
|
||||
yield token.with_reference(
|
||||
ReferencedBy(
|
||||
name=variable_name,
|
||||
location=ref_location,
|
||||
length=ref_length,
|
||||
)
|
||||
ref_code = token.code
|
||||
for _token in variable_tokens:
|
||||
yield _token.with_reference(
|
||||
ReferencedBy(variable_name, ref_location, ref_length, ref_code)
|
||||
)
|
||||
else:
|
||||
_unresolved(variable_name, variables.keys(), token)
|
||||
@@ -336,6 +331,7 @@ def parse(
|
||||
css: str,
|
||||
path: str | PurePath,
|
||||
variables: dict[str, str] | None = None,
|
||||
variable_tokens: dict[str, list[Token]] | None = None,
|
||||
is_default_rules: bool = False,
|
||||
tie_breaker: int = 0,
|
||||
) -> Iterable[RuleSet]:
|
||||
@@ -349,7 +345,11 @@ def parse(
|
||||
is_default_rules (bool): True if the rules we're extracting are
|
||||
default (i.e. in Widget.DEFAULT_CSS) rules. False if they're from user defined CSS.
|
||||
"""
|
||||
variable_tokens = tokenize_values(variables or {})
|
||||
|
||||
reference_tokens = tokenize_values(variables) if variables is not None else {}
|
||||
if variable_tokens:
|
||||
reference_tokens.update(variable_tokens)
|
||||
|
||||
tokens = iter(substitute_references(tokenize(css, path), variable_tokens))
|
||||
while True:
|
||||
token = next(tokens, None)
|
||||
|
||||
@@ -16,6 +16,7 @@ from ..color import Color
|
||||
from ..geometry import Offset, Spacing
|
||||
from ._style_properties import (
|
||||
AlignProperty,
|
||||
BooleanProperty,
|
||||
BorderProperty,
|
||||
BoxProperty,
|
||||
ColorProperty,
|
||||
@@ -83,6 +84,7 @@ class RulesMap(TypedDict, total=False):
|
||||
visibility: Visibility
|
||||
layout: "Layout"
|
||||
|
||||
auto_color: bool
|
||||
color: Color
|
||||
background: Color
|
||||
text_style: Style
|
||||
@@ -183,6 +185,7 @@ class StylesBase(ABC):
|
||||
"min_height",
|
||||
"max_width",
|
||||
"max_height",
|
||||
"auto_color",
|
||||
"color",
|
||||
"background",
|
||||
"opacity",
|
||||
@@ -202,6 +205,7 @@ class StylesBase(ABC):
|
||||
visibility = StringEnumProperty(VALID_VISIBILITY, "visible")
|
||||
layout = LayoutProperty()
|
||||
|
||||
auto_color = BooleanProperty(default=False)
|
||||
color = ColorProperty(Color(255, 255, 255))
|
||||
background = ColorProperty(Color(0, 0, 0, 0), background=True)
|
||||
text_style = StyleFlagsProperty()
|
||||
|
||||
@@ -2,7 +2,6 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from functools import partial
|
||||
from operator import itemgetter
|
||||
from pathlib import Path, PurePath
|
||||
from typing import Iterable, NamedTuple, cast
|
||||
@@ -39,14 +38,9 @@ class StylesheetParseError(StylesheetError):
|
||||
|
||||
|
||||
class StylesheetErrors:
|
||||
def __init__(
|
||||
self, rules: list[RuleSet], variables: dict[str, str] | None = None
|
||||
) -> None:
|
||||
def __init__(self, rules: list[RuleSet]) -> None:
|
||||
self.rules = rules
|
||||
self.variables: dict[str, str] = {}
|
||||
self._css_variables: dict[str, list[Token]] = {}
|
||||
if variables:
|
||||
self.set_variables(variables)
|
||||
|
||||
@classmethod
|
||||
def _get_snippet(cls, code: str, line_no: int) -> RenderableType:
|
||||
@@ -61,11 +55,6 @@ class StylesheetErrors:
|
||||
)
|
||||
return syntax
|
||||
|
||||
def set_variables(self, variable_map: dict[str, str]) -> None:
|
||||
"""Pre-populate CSS variables."""
|
||||
self.variables.update(variable_map)
|
||||
self._css_variables = tokenize_values(self.variables)
|
||||
|
||||
def __rich_console__(
|
||||
self, console: Console, options: ConsoleOptions
|
||||
) -> RenderResult:
|
||||
@@ -105,7 +94,10 @@ class StylesheetErrors:
|
||||
title = Text.assemble(Text("Error at ", style="bold red"), path_text)
|
||||
yield ""
|
||||
yield Panel(
|
||||
self._get_snippet(token.code, line_no),
|
||||
self._get_snippet(
|
||||
token.referenced_by.code if token.referenced_by else token.code,
|
||||
line_no,
|
||||
),
|
||||
title=title,
|
||||
title_align="left",
|
||||
border_style="red",
|
||||
@@ -138,13 +130,20 @@ class Stylesheet:
|
||||
def __init__(self, *, variables: dict[str, str] | None = None) -> None:
|
||||
self._rules: list[RuleSet] = []
|
||||
self._rules_map: dict[str, list[RuleSet]] | None = None
|
||||
self.variables = variables or {}
|
||||
self._variables = variables or {}
|
||||
self.__variable_tokens: dict[str, list[Token]] | None = None
|
||||
self.source: dict[str, CssSource] = {}
|
||||
self._require_parse = False
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
yield list(self.source.keys())
|
||||
|
||||
@property
|
||||
def _variable_tokens(self) -> dict[str, list[Token]]:
|
||||
if self.__variable_tokens is None:
|
||||
self.__variable_tokens = tokenize_values(self._variables)
|
||||
return self.__variable_tokens
|
||||
|
||||
@property
|
||||
def rules(self) -> list[RuleSet]:
|
||||
"""List of rule sets.
|
||||
@@ -183,7 +182,7 @@ class Stylesheet:
|
||||
Returns:
|
||||
Stylesheet: New stylesheet.
|
||||
"""
|
||||
stylesheet = Stylesheet(variables=self.variables.copy())
|
||||
stylesheet = Stylesheet(variables=self._variables.copy())
|
||||
stylesheet.source = self.source.copy()
|
||||
return stylesheet
|
||||
|
||||
@@ -193,7 +192,8 @@ class Stylesheet:
|
||||
Args:
|
||||
variables (dict[str, str]): A mapping of name to variable.
|
||||
"""
|
||||
self.variables = variables
|
||||
self._variables = variables
|
||||
self._variables_tokens = None
|
||||
|
||||
def _parse_rules(
|
||||
self,
|
||||
@@ -222,7 +222,7 @@ class Stylesheet:
|
||||
parse(
|
||||
css,
|
||||
path,
|
||||
variables=self.variables,
|
||||
variable_tokens=self._variable_tokens,
|
||||
is_default_rules=is_default_rules,
|
||||
tie_breaker=tie_breaker,
|
||||
)
|
||||
@@ -317,7 +317,7 @@ class Stylesheet:
|
||||
|
||||
"""
|
||||
# Do this in a fresh Stylesheet so if there are errors we don't break self.
|
||||
stylesheet = Stylesheet(variables=self.variables)
|
||||
stylesheet = Stylesheet(variables=self._variables)
|
||||
for path, (css, is_defaults, tie_breaker) in self.source.items():
|
||||
stylesheet.add_source(
|
||||
css, path, is_default_css=is_defaults, tie_breaker=tie_breaker
|
||||
|
||||
@@ -118,6 +118,7 @@ class ReferencedBy(NamedTuple):
|
||||
name: str
|
||||
location: tuple[int, int]
|
||||
length: int
|
||||
code: str
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
@@ -209,6 +210,7 @@ class Tokenizer:
|
||||
message,
|
||||
)
|
||||
iter_groups = iter(match.groups())
|
||||
|
||||
next(iter_groups)
|
||||
|
||||
for name, value in zip(expect.names, iter_groups):
|
||||
|
||||
@@ -15,7 +15,7 @@ NUMBER_OF_SHADES = 3
|
||||
# Where no content exists
|
||||
DEFAULT_DARK_BACKGROUND = "#121212"
|
||||
# What text usually goes on top off
|
||||
DEFAULT_DARK_SURFACE = "#292929"
|
||||
DEFAULT_DARK_SURFACE = "#121212"
|
||||
|
||||
DEFAULT_LIGHT_SURFACE = "#f5f5f5"
|
||||
DEFAULT_LIGHT_BACKGROUND = "#efefef"
|
||||
@@ -38,6 +38,7 @@ class ColorSystem:
|
||||
"secondary-background",
|
||||
"surface",
|
||||
"panel",
|
||||
"boost",
|
||||
"warning",
|
||||
"error",
|
||||
"success",
|
||||
@@ -55,6 +56,7 @@ class ColorSystem:
|
||||
background: str | None = None,
|
||||
surface: str | None = None,
|
||||
panel: str | None = None,
|
||||
boost: str | None = None,
|
||||
dark: bool = False,
|
||||
luminosity_spread: float = 0.15,
|
||||
text_alpha: float = 0.95,
|
||||
@@ -73,6 +75,7 @@ class ColorSystem:
|
||||
self.background = parse(background)
|
||||
self.surface = parse(surface)
|
||||
self.panel = parse(panel)
|
||||
self.boost = parse(boost)
|
||||
self._dark = dark
|
||||
self._luminosity_spread = luminosity_spread
|
||||
self._text_alpha = text_alpha
|
||||
@@ -121,8 +124,12 @@ class ColorSystem:
|
||||
background = self.background or Color.parse(DEFAULT_LIGHT_BACKGROUND)
|
||||
surface = self.surface or Color.parse(DEFAULT_LIGHT_SURFACE)
|
||||
|
||||
boost = self.boost or background.get_contrast_text(1.0).with_alpha(0.07)
|
||||
|
||||
if self.panel is None:
|
||||
panel = surface.blend(primary, luminosity_spread)
|
||||
if dark:
|
||||
panel += boost
|
||||
else:
|
||||
panel = self.panel
|
||||
|
||||
@@ -153,6 +160,7 @@ class ColorSystem:
|
||||
("secondary-background", secondary),
|
||||
("background", background),
|
||||
("panel", panel),
|
||||
("boost", boost),
|
||||
("surface", surface),
|
||||
("warning", warning),
|
||||
("error", error),
|
||||
@@ -178,14 +186,10 @@ class ColorSystem:
|
||||
else:
|
||||
shade_color = color.lighten(luminosity_delta)
|
||||
colors[f"{name}{shade_name}"] = shade_color.hex
|
||||
for fade in range(3):
|
||||
text_color = shade_color.get_contrast_text(text_alpha)
|
||||
if fade > 0:
|
||||
text_color = text_color.blend(shade_color, fade * 0.1 + 0.15)
|
||||
on_name = f"text-{name}{shade_name}-fade-{fade}"
|
||||
else:
|
||||
on_name = f"text-{name}{shade_name}"
|
||||
colors[on_name] = text_color.hex
|
||||
|
||||
colors["text"] = "auto 95%"
|
||||
colors["text-muted"] = "auto 80%"
|
||||
colors["text-disabled"] = "auto 60%"
|
||||
|
||||
return colors
|
||||
|
||||
@@ -206,16 +210,12 @@ def show_design(light: ColorSystem, dark: ColorSystem) -> Table:
|
||||
def make_shades(system: ColorSystem):
|
||||
colors = system.generate()
|
||||
for name in system.shades:
|
||||
background = colors[name]
|
||||
foreground = colors[f"text-{name}"]
|
||||
text = Text(f"{background} ", style=f"{foreground} on {background}")
|
||||
for fade in range(3):
|
||||
foreground = colors[
|
||||
f"text-{name}-fade-{fade}" if fade else f"text-{name}"
|
||||
]
|
||||
text.append(f"{name} ", style=f"{foreground} on {background}")
|
||||
background = Color.parse(colors[name]).with_alpha(1.0)
|
||||
foreground = background + background.get_contrast_text(0.9)
|
||||
|
||||
yield Padding(text, 1, style=f"{foreground} on {background}")
|
||||
text = Text(name)
|
||||
|
||||
yield Padding(text, 1, style=f"{foreground.hex6} on {background.hex6}")
|
||||
|
||||
table = Table(box=None, expand=True)
|
||||
table.add_column("Light", justify="center")
|
||||
|
||||
@@ -497,6 +497,8 @@ class DOMNode(MessagePump):
|
||||
if styles.has_rule("color"):
|
||||
color = styles.color
|
||||
style += styles.text_style
|
||||
if styles.has_rule("auto_color") and styles.auto_color:
|
||||
color = background.get_contrast_text(color.a)
|
||||
style += Style.from_color(
|
||||
(background + color).rich_color, background.rich_color
|
||||
)
|
||||
@@ -534,7 +536,11 @@ class DOMNode(MessagePump):
|
||||
background += styles.background
|
||||
if styles.has_rule("color"):
|
||||
base_color = color
|
||||
if styles.auto_color:
|
||||
color = background.get_contrast_text(color.a)
|
||||
else:
|
||||
color = styles.color
|
||||
|
||||
return (base_background, base_color, background, color)
|
||||
|
||||
@property
|
||||
|
||||
@@ -33,6 +33,7 @@ class Screen(Widget):
|
||||
Screen {
|
||||
layout: vertical;
|
||||
overflow-y: auto;
|
||||
background: $surface;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
@@ -1070,7 +1070,9 @@ class Widget(DOMNode):
|
||||
|
||||
while isinstance(widget.parent, Widget) and widget is not self:
|
||||
container = widget.parent
|
||||
scroll_offset = container.scroll_to_region(region, animate=animate)
|
||||
scroll_offset = container.scroll_to_region(
|
||||
region, spacing=widget.parent.gutter, animate=animate
|
||||
)
|
||||
if scroll_offset:
|
||||
scrolled = True
|
||||
|
||||
@@ -1108,6 +1110,10 @@ class Widget(DOMNode):
|
||||
window = self.content_region.at_offset(self.scroll_offset)
|
||||
if spacing is not None:
|
||||
window = window.shrink(spacing)
|
||||
|
||||
if window in region:
|
||||
return Offset()
|
||||
|
||||
delta_x, delta_y = Region.get_scroll_to_visible(window, region)
|
||||
scroll_x, scroll_y = self.scroll_offset
|
||||
delta = Offset(
|
||||
|
||||
@@ -33,10 +33,9 @@ class Button(Widget, can_focus=True):
|
||||
Button {
|
||||
width: auto;
|
||||
min-width: 16;
|
||||
width: auto;
|
||||
height: 3;
|
||||
background: $panel;
|
||||
color: $text-panel;
|
||||
color: $text;
|
||||
border: none;
|
||||
border-top: tall $panel-lighten-2;
|
||||
border-bottom: tall $panel-darken-3;
|
||||
@@ -56,7 +55,7 @@ class Button(Widget, can_focus=True):
|
||||
Button:hover {
|
||||
border-top: tall $panel-lighten-1;
|
||||
background: $panel-darken-2;
|
||||
color: $text-panel-darken-2;
|
||||
color: $text;
|
||||
}
|
||||
|
||||
Button.-active {
|
||||
@@ -69,7 +68,7 @@ class Button(Widget, can_focus=True):
|
||||
/* Primary variant */
|
||||
Button.-primary {
|
||||
background: $primary;
|
||||
color: $text-primary;
|
||||
color: $text;
|
||||
border-top: tall $primary-lighten-3;
|
||||
border-bottom: tall $primary-darken-3;
|
||||
|
||||
@@ -77,8 +76,8 @@ class Button(Widget, can_focus=True):
|
||||
|
||||
Button.-primary:hover {
|
||||
background: $primary-darken-2;
|
||||
color: $text-primary-darken-2;
|
||||
|
||||
color: $text;
|
||||
border-top: tall $primary-lighten-2;
|
||||
}
|
||||
|
||||
Button.-primary.-active {
|
||||
@@ -91,14 +90,14 @@ class Button(Widget, can_focus=True):
|
||||
/* Success variant */
|
||||
Button.-success {
|
||||
background: $success;
|
||||
color: $text-success;
|
||||
color: $text;
|
||||
border-top: tall $success-lighten-2;
|
||||
border-bottom: tall $success-darken-3;
|
||||
}
|
||||
|
||||
Button.-success:hover {
|
||||
background: $success-darken-2;
|
||||
color: $text-success-darken-2;
|
||||
color: $text;
|
||||
}
|
||||
|
||||
Button.-success.-active {
|
||||
@@ -111,14 +110,14 @@ class Button(Widget, can_focus=True):
|
||||
/* Warning variant */
|
||||
Button.-warning {
|
||||
background: $warning;
|
||||
color: $text-warning;
|
||||
color: $text;
|
||||
border-top: tall $warning-lighten-2;
|
||||
border-bottom: tall $warning-darken-3;
|
||||
}
|
||||
|
||||
Button.-warning:hover {
|
||||
background: $warning-darken-2;
|
||||
color: $text-warning-darken-1;
|
||||
color: $text;
|
||||
|
||||
}
|
||||
|
||||
@@ -132,7 +131,7 @@ class Button(Widget, can_focus=True):
|
||||
/* Error variant */
|
||||
Button.-error {
|
||||
background: $error;
|
||||
color: $text-error;
|
||||
color: $text;
|
||||
border-top: tall $error-lighten-2;
|
||||
border-bottom: tall $error-darken-3;
|
||||
|
||||
@@ -140,7 +139,7 @@ class Button(Widget, can_focus=True):
|
||||
|
||||
Button.-error:hover {
|
||||
background: $error-darken-1;
|
||||
color: $text-error-darken-2;
|
||||
color: $text;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -109,17 +109,17 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
DEFAULT_CSS = """
|
||||
DataTable {
|
||||
background: $surface;
|
||||
color: $text-surface;
|
||||
color: $text;
|
||||
}
|
||||
DataTable > .datatable--header {
|
||||
text-style: bold;
|
||||
background: $primary;
|
||||
color: $text-primary;
|
||||
color: $text;
|
||||
}
|
||||
DataTable > .datatable--fixed {
|
||||
text-style: bold;
|
||||
background: $primary;
|
||||
color: $text-primary;
|
||||
color: $text;
|
||||
}
|
||||
|
||||
DataTable > .datatable--odd-row {
|
||||
@@ -132,7 +132,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
|
||||
DataTable > .datatable--cursor {
|
||||
background: $secondary;
|
||||
color: $text-secondary;
|
||||
color: $text;
|
||||
}
|
||||
|
||||
.-dark-mode DataTable > .datatable--even-row {
|
||||
@@ -557,7 +557,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
|
||||
def _scroll_cursor_in_to_view(self, animate: bool = False) -> None:
|
||||
region = self._get_cell_region(self.cursor_row, self.cursor_column)
|
||||
spacing = self._get_cell_border()
|
||||
spacing = self._get_cell_border() + self.scrollbar_gutter
|
||||
self.scroll_to_region(region, animate=animate, spacing=spacing)
|
||||
|
||||
def on_click(self, event: events.Click) -> None:
|
||||
|
||||
@@ -74,7 +74,6 @@ class DirectoryTree(TreeControl[DirEntry]):
|
||||
label.stylize("bold")
|
||||
icon = "📂" if expanded else "📁"
|
||||
else:
|
||||
|
||||
icon = "📄"
|
||||
label.highlight_regex(r"\..*$", "italic")
|
||||
|
||||
|
||||
@@ -16,25 +16,22 @@ class Footer(Widget):
|
||||
DEFAULT_CSS = """
|
||||
Footer {
|
||||
background: $accent;
|
||||
color: $text-accent;
|
||||
color: $text;
|
||||
dock: bottom;
|
||||
height: 1;
|
||||
}
|
||||
Footer > .footer--highlight {
|
||||
background: $accent-darken-1;
|
||||
color: $text-accent-darken-1;
|
||||
}
|
||||
|
||||
Footer > .footer--highlight-key {
|
||||
background: $secondary;
|
||||
color: $text-secondary;
|
||||
text-style: bold;
|
||||
}
|
||||
|
||||
Footer > .footer--key {
|
||||
text-style: bold;
|
||||
background: $accent-darken-2;
|
||||
color: $text-accent-darken-2;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class HeaderClock(Widget):
|
||||
width: 10;
|
||||
padding: 0 1;
|
||||
background: $secondary-background-lighten-1;
|
||||
color: $text-secondary-background;
|
||||
color: $text;
|
||||
text-opacity: 85%;
|
||||
content-align: center middle;
|
||||
}
|
||||
@@ -76,7 +76,7 @@ class Header(Widget):
|
||||
dock: top;
|
||||
width: 100%;
|
||||
background: $secondary-background;
|
||||
color: $text-secondary-background;
|
||||
color: $text;
|
||||
height: 1;
|
||||
}
|
||||
Header.tall {
|
||||
|
||||
@@ -165,7 +165,7 @@ class TreeControl(Generic[NodeDataType], Static, can_focus=True):
|
||||
DEFAULT_CSS = """
|
||||
TreeControl {
|
||||
background: $surface;
|
||||
color: $text-surface;
|
||||
color: $text;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -177,7 +177,6 @@ class TreeControl(Generic[NodeDataType], Static, can_focus=True):
|
||||
TreeControl > .tree--guides-highlight {
|
||||
color: $success;
|
||||
text-style: uu;
|
||||
|
||||
}
|
||||
|
||||
TreeControl > .tree--guides-cursor {
|
||||
@@ -186,12 +185,12 @@ class TreeControl(Generic[NodeDataType], Static, can_focus=True):
|
||||
}
|
||||
|
||||
TreeControl > .tree--labels {
|
||||
color: $text-panel;
|
||||
color: $text;
|
||||
}
|
||||
|
||||
TreeControl > .tree--cursor {
|
||||
background: $secondary;
|
||||
color: $text-secondary;
|
||||
color: $text;
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
@@ -37,7 +37,7 @@ class Welcome(Static):
|
||||
Welcome Container {
|
||||
padding: 1;
|
||||
background: $panel;
|
||||
color: $text-panel;
|
||||
color: $text;
|
||||
}
|
||||
|
||||
Welcome #text {
|
||||
|
||||
@@ -97,7 +97,9 @@ class TestVariableReferenceSubstitution:
|
||||
path="",
|
||||
code=css,
|
||||
location=(0, 4),
|
||||
referenced_by=ReferencedBy(name="x", location=(0, 28), length=2),
|
||||
referenced_by=ReferencedBy(
|
||||
name="x", location=(0, 28), length=2, code=css
|
||||
),
|
||||
),
|
||||
Token(
|
||||
name="declaration_end",
|
||||
@@ -191,7 +193,9 @@ class TestVariableReferenceSubstitution:
|
||||
path="",
|
||||
code=css,
|
||||
location=(0, 3),
|
||||
referenced_by=ReferencedBy(name="x", location=(0, 27), length=2),
|
||||
referenced_by=ReferencedBy(
|
||||
name="x", location=(0, 27), length=2, code=css
|
||||
),
|
||||
),
|
||||
Token(
|
||||
name="declaration_end",
|
||||
@@ -273,7 +277,9 @@ class TestVariableReferenceSubstitution:
|
||||
path="",
|
||||
code=css,
|
||||
location=(0, 4),
|
||||
referenced_by=ReferencedBy(name="x", location=(1, 4), length=2),
|
||||
referenced_by=ReferencedBy(
|
||||
name="x", location=(1, 4), length=2, code=css
|
||||
),
|
||||
),
|
||||
Token(
|
||||
name="variable_value_end",
|
||||
@@ -337,7 +343,9 @@ class TestVariableReferenceSubstitution:
|
||||
path="",
|
||||
code=css,
|
||||
location=(0, 4),
|
||||
referenced_by=ReferencedBy(name="y", location=(2, 17), length=2),
|
||||
referenced_by=ReferencedBy(
|
||||
name="y", location=(2, 17), length=2, code=css
|
||||
),
|
||||
),
|
||||
Token(
|
||||
name="whitespace",
|
||||
@@ -446,7 +454,9 @@ class TestVariableReferenceSubstitution:
|
||||
path="",
|
||||
code=css,
|
||||
location=(0, 4),
|
||||
referenced_by=ReferencedBy(name="x", location=(1, 6), length=2),
|
||||
referenced_by=ReferencedBy(
|
||||
name="x", location=(1, 6), length=2, code=css
|
||||
),
|
||||
),
|
||||
Token(
|
||||
name="whitespace",
|
||||
@@ -454,7 +464,9 @@ class TestVariableReferenceSubstitution:
|
||||
path="",
|
||||
code=css,
|
||||
location=(0, 5),
|
||||
referenced_by=ReferencedBy(name="x", location=(1, 6), length=2),
|
||||
referenced_by=ReferencedBy(
|
||||
name="x", location=(1, 6), length=2, code=css
|
||||
),
|
||||
),
|
||||
Token(
|
||||
name="number",
|
||||
@@ -462,7 +474,9 @@ class TestVariableReferenceSubstitution:
|
||||
path="",
|
||||
code=css,
|
||||
location=(0, 6),
|
||||
referenced_by=ReferencedBy(name="x", location=(1, 6), length=2),
|
||||
referenced_by=ReferencedBy(
|
||||
name="x", location=(1, 6), length=2, code=css
|
||||
),
|
||||
),
|
||||
Token(
|
||||
name="whitespace",
|
||||
@@ -542,7 +556,9 @@ class TestVariableReferenceSubstitution:
|
||||
path="",
|
||||
code=css,
|
||||
location=(1, 4),
|
||||
referenced_by=ReferencedBy(name="y", location=(2, 17), length=2),
|
||||
referenced_by=ReferencedBy(
|
||||
name="y", location=(2, 17), length=2, code=css
|
||||
),
|
||||
),
|
||||
Token(
|
||||
name="whitespace",
|
||||
@@ -550,7 +566,9 @@ class TestVariableReferenceSubstitution:
|
||||
path="",
|
||||
code=css,
|
||||
location=(1, 5),
|
||||
referenced_by=ReferencedBy(name="y", location=(2, 17), length=2),
|
||||
referenced_by=ReferencedBy(
|
||||
name="y", location=(2, 17), length=2, code=css
|
||||
),
|
||||
),
|
||||
Token(
|
||||
name="number",
|
||||
@@ -558,7 +576,9 @@ class TestVariableReferenceSubstitution:
|
||||
path="",
|
||||
code=css,
|
||||
location=(0, 4),
|
||||
referenced_by=ReferencedBy(name="y", location=(2, 17), length=2),
|
||||
referenced_by=ReferencedBy(
|
||||
name="y", location=(2, 17), length=2, code=css
|
||||
),
|
||||
),
|
||||
Token(
|
||||
name="whitespace",
|
||||
@@ -566,7 +586,9 @@ class TestVariableReferenceSubstitution:
|
||||
path="",
|
||||
code=css,
|
||||
location=(0, 5),
|
||||
referenced_by=ReferencedBy(name="y", location=(2, 17), length=2),
|
||||
referenced_by=ReferencedBy(
|
||||
name="y", location=(2, 17), length=2, code=css
|
||||
),
|
||||
),
|
||||
Token(
|
||||
name="number",
|
||||
@@ -574,7 +596,9 @@ class TestVariableReferenceSubstitution:
|
||||
path="",
|
||||
code=css,
|
||||
location=(0, 6),
|
||||
referenced_by=ReferencedBy(name="y", location=(2, 17), length=2),
|
||||
referenced_by=ReferencedBy(
|
||||
name="y", location=(2, 17), length=2, code=css
|
||||
),
|
||||
),
|
||||
Token(
|
||||
name="whitespace",
|
||||
@@ -582,7 +606,9 @@ class TestVariableReferenceSubstitution:
|
||||
path="",
|
||||
code=css,
|
||||
location=(1, 8),
|
||||
referenced_by=ReferencedBy(name="y", location=(2, 17), length=2),
|
||||
referenced_by=ReferencedBy(
|
||||
name="y", location=(2, 17), length=2, code=css
|
||||
),
|
||||
),
|
||||
Token(
|
||||
name="number",
|
||||
@@ -590,7 +616,9 @@ class TestVariableReferenceSubstitution:
|
||||
path="",
|
||||
code=css,
|
||||
location=(1, 9),
|
||||
referenced_by=ReferencedBy(name="y", location=(2, 17), length=2),
|
||||
referenced_by=ReferencedBy(
|
||||
name="y", location=(2, 17), length=2, code=css
|
||||
),
|
||||
),
|
||||
Token(
|
||||
name="whitespace",
|
||||
@@ -715,7 +743,9 @@ class TestVariableReferenceSubstitution:
|
||||
path="",
|
||||
code=css,
|
||||
location=(0, 4),
|
||||
referenced_by=ReferencedBy(name="x", location=(1, 20), length=2),
|
||||
referenced_by=ReferencedBy(
|
||||
name="x", location=(1, 20), length=2, code=css
|
||||
),
|
||||
),
|
||||
Token(
|
||||
name="declaration_end",
|
||||
@@ -845,7 +875,9 @@ class TestVariableReferenceSubstitution:
|
||||
path="",
|
||||
code=css,
|
||||
location=(0, 7),
|
||||
referenced_by=ReferencedBy(name="x", location=(0, 26), length=2),
|
||||
referenced_by=ReferencedBy(
|
||||
name="x", location=(0, 26), length=2, code=css
|
||||
),
|
||||
),
|
||||
Token(
|
||||
name="declaration_set_end",
|
||||
@@ -1134,7 +1166,9 @@ class TestParsePadding:
|
||||
|
||||
|
||||
class TestParseTextAlign:
|
||||
@pytest.mark.parametrize("valid_align", ["left", "start", "center", "right", "end", "justify"])
|
||||
@pytest.mark.parametrize(
|
||||
"valid_align", ["left", "start", "center", "right", "end", "justify"]
|
||||
)
|
||||
def test_text_align(self, valid_align):
|
||||
css = f"#foo {{ text-align: {valid_align} }}"
|
||||
stylesheet = Stylesheet()
|
||||
|
||||
@@ -189,8 +189,6 @@ async def test_composition_of_vertical_container_with_children(
|
||||
"outer",
|
||||
"hkey",
|
||||
"vkey",
|
||||
"tall",
|
||||
"wide",
|
||||
]
|
||||
],
|
||||
),
|
||||
|
||||