mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
new layout
This commit is contained in:
@@ -41,6 +41,7 @@ App > Screen {
|
||||
background: $primary-darken-2;
|
||||
color: $text-primary-darken-2 ;
|
||||
border-right: outer $primary-darken-3;
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
#sidebar .user {
|
||||
@@ -48,19 +49,21 @@ App > Screen {
|
||||
background: $primary-darken-1;
|
||||
color: $text-primary-darken-1;
|
||||
border-right: outer $primary-darken-3;
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
#sidebar .content {
|
||||
background: $primary;
|
||||
color: $text-primary;
|
||||
border-right: outer $primary-darken-3;
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
#header {
|
||||
color: $text-primary-darken-1;
|
||||
background: $primary-darken-1;
|
||||
height: 3
|
||||
|
||||
height: 3;
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
#content {
|
||||
@@ -84,6 +87,7 @@ Tweet {
|
||||
border: wide $panel-darken-2;
|
||||
overflow-y: scroll;
|
||||
align-horizontal: center;
|
||||
|
||||
}
|
||||
|
||||
.scrollable {
|
||||
@@ -152,6 +156,7 @@ TweetBody {
|
||||
background: $accent;
|
||||
height: 1;
|
||||
border-top: hkey $accent-darken-2;
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
|
||||
@@ -165,6 +170,7 @@ OptionItem {
|
||||
transition: background 100ms linear;
|
||||
border-right: outer $primary-darken-2;
|
||||
border-left: hidden;
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
OptionItem:hover {
|
||||
|
||||
@@ -66,7 +66,7 @@ class Tweet(Widget):
|
||||
|
||||
class OptionItem(Widget):
|
||||
def render(self) -> Text:
|
||||
return Align.center(Text("Option", justify="center"), vertical="middle")
|
||||
return Text("Option")
|
||||
|
||||
|
||||
class Error(Widget):
|
||||
@@ -95,10 +95,9 @@ class BasicApp(App):
|
||||
"""Build layout here."""
|
||||
self.mount(
|
||||
header=Static(
|
||||
Align.center(
|
||||
"[b]This is a [u]Textual[/u] app, running in the terminal",
|
||||
vertical="middle",
|
||||
)
|
||||
Text.from_markup(
|
||||
"[b]This is a [u]Textual[/u] app, running in the terminal"
|
||||
),
|
||||
),
|
||||
content=Widget(
|
||||
Tweet(
|
||||
@@ -140,4 +139,7 @@ class BasicApp(App):
|
||||
self.panic(self.tree)
|
||||
|
||||
|
||||
BasicApp.run(css_file="basic.css", watch_css=True, log="textual.log")
|
||||
app = BasicApp(css_file="basic.css", watch_css=True, log="textual.log")
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
||||
|
||||
0
sandbox/buttons.css
Normal file
0
sandbox/buttons.css
Normal file
24
sandbox/buttons.py
Normal file
24
sandbox/buttons.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from textual.app import App, ComposeResult
|
||||
|
||||
from textual.widgets import Button
|
||||
from textual import layout
|
||||
|
||||
|
||||
class ButtonsApp(App[str]):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield layout.Vertical(
|
||||
Button("foo", id="foo"),
|
||||
Button("bar", id="bar"),
|
||||
Button("baz", id="baz"),
|
||||
)
|
||||
|
||||
def handle_pressed(self, event: Button.Pressed) -> None:
|
||||
self.app.bell()
|
||||
self.exit(event.button.id)
|
||||
|
||||
|
||||
app = ButtonsApp(log="textual.log")
|
||||
|
||||
if __name__ == "__main__":
|
||||
result = app.run()
|
||||
print(repr(result))
|
||||
@@ -56,4 +56,7 @@ class BasicApp(App):
|
||||
sys.stdout.write("abcdef")
|
||||
|
||||
|
||||
BasicApp.run(css_file="uber.css", log="textual.log", log_verbosity=1)
|
||||
app = BasicApp(css_file="uber.css", log="textual.log", log_verbosity=1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
ANSI_COLOR_TO_RGB: dict[str, tuple[int, int, int]] = {
|
||||
COLOR_NAME_TO_RGB: dict[str, tuple[int, int, int]] = {
|
||||
"black": (0, 0, 0),
|
||||
"red": (128, 0, 0),
|
||||
"green": (0, 128, 0),
|
||||
|
||||
@@ -9,7 +9,7 @@ import warnings
|
||||
from asyncio import AbstractEventLoop
|
||||
from contextlib import redirect_stdout
|
||||
from time import perf_counter
|
||||
from typing import Any, Iterable, TextIO, Type, TYPE_CHECKING
|
||||
from typing import Any, Generic, Iterable, TextIO, Type, TypeVar, TYPE_CHECKING
|
||||
|
||||
import rich
|
||||
import rich.repr
|
||||
@@ -67,6 +67,8 @@ DEFAULT_COLORS = ColorSystem(
|
||||
dark_surface="#292929",
|
||||
)
|
||||
|
||||
ComposeResult = Iterable[Widget]
|
||||
|
||||
|
||||
class AppError(Exception):
|
||||
pass
|
||||
@@ -76,8 +78,11 @@ class ActionError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
ReturnType = TypeVar("ReturnType")
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
class App(DOMNode):
|
||||
class App(Generic[ReturnType], DOMNode):
|
||||
"""The base class for Textual Applications"""
|
||||
|
||||
css = ""
|
||||
@@ -159,6 +164,8 @@ class App(DOMNode):
|
||||
|
||||
self.devtools = DevtoolsClient()
|
||||
|
||||
self._return_value: ReturnType | None = None
|
||||
|
||||
super().__init__()
|
||||
|
||||
title: Reactive[str] = Reactive("Textual")
|
||||
@@ -166,6 +173,14 @@ class App(DOMNode):
|
||||
background: Reactive[str] = Reactive("black")
|
||||
dark = Reactive(False)
|
||||
|
||||
def exit(self, result: ReturnType | None = None) -> None:
|
||||
self._return_value = result
|
||||
self.close_messages_no_wait()
|
||||
|
||||
def compose(self) -> Iterable[Widget]:
|
||||
return
|
||||
yield
|
||||
|
||||
def get_css_variables(self) -> dict[str, str]:
|
||||
"""Get a mapping of variables used to pre-populate CSS.
|
||||
|
||||
@@ -284,27 +299,9 @@ class App(DOMNode):
|
||||
keys, action, description, show=show, key_display=key_display
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def run(
|
||||
cls,
|
||||
console: Console | None = None,
|
||||
screen: bool = True,
|
||||
driver: Type[Driver] | None = None,
|
||||
loop: AbstractEventLoop | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Run the app.
|
||||
|
||||
Args:
|
||||
console (Console, optional): Console object. Defaults to None.
|
||||
screen (bool, optional): Enable application mode. Defaults to True.
|
||||
driver (Type[Driver], optional): Driver class or None for default. Defaults to None.
|
||||
loop (AbstractEventLoop): Event loop to run the application on. If not specified, uvloop will be used.
|
||||
"""
|
||||
|
||||
def run(self, loop: AbstractEventLoop | None = None) -> ReturnType | None:
|
||||
async def run_app() -> None:
|
||||
app = cls(screen=screen, driver_class=driver, **kwargs)
|
||||
await app.process_messages()
|
||||
await self.process_messages()
|
||||
|
||||
if loop:
|
||||
asyncio.set_event_loop(loop)
|
||||
@@ -322,6 +319,8 @@ class App(DOMNode):
|
||||
finally:
|
||||
event_loop.close()
|
||||
|
||||
return self._return_value
|
||||
|
||||
async def _on_css_change(self) -> None:
|
||||
"""Called when the CSS changes (if watch_css is True)."""
|
||||
if self.css_file is not None:
|
||||
@@ -341,6 +340,9 @@ class App(DOMNode):
|
||||
self.stylesheet.update(self)
|
||||
self.screen.refresh(layout=True)
|
||||
|
||||
def render(self) -> RenderableType:
|
||||
return ""
|
||||
|
||||
def query(self, selector: str | None = None) -> DOMQuery:
|
||||
"""Get a DOM query in the current screen.
|
||||
|
||||
@@ -547,6 +549,12 @@ class App(DOMNode):
|
||||
if self._log_file is not None:
|
||||
self._log_file.close()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
widgets = list(self.compose())
|
||||
if widgets:
|
||||
self.mount(*widgets)
|
||||
self.screen.refresh()
|
||||
|
||||
async def on_idle(self) -> None:
|
||||
"""Perform actions when there are no messages in the queue."""
|
||||
if self._require_styles_update:
|
||||
|
||||
@@ -24,7 +24,7 @@ from rich.style import Style
|
||||
from rich.text import Text
|
||||
|
||||
|
||||
from ._color_constants import ANSI_COLOR_TO_RGB
|
||||
from ._color_constants import COLOR_NAME_TO_RGB
|
||||
from .geometry import clamp
|
||||
|
||||
|
||||
@@ -123,6 +123,10 @@ class Color(NamedTuple):
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
def is_transparent(self) -> bool:
|
||||
return self.a == 0
|
||||
|
||||
@property
|
||||
def clamped(self) -> Color:
|
||||
"""Get a color with all components saturated to maximum and minimum values."""
|
||||
@@ -253,7 +257,9 @@ class Color(NamedTuple):
|
||||
"""
|
||||
if isinstance(color_text, Color):
|
||||
return color_text
|
||||
ansi_color = ANSI_COLOR_TO_RGB.get(color_text)
|
||||
if color_text == "transparent":
|
||||
return TRANSPARENT
|
||||
ansi_color = COLOR_NAME_TO_RGB.get(color_text)
|
||||
if ansi_color is not None:
|
||||
return cls(*ansi_color)
|
||||
color_match = RE_COLOR.match(color_text)
|
||||
@@ -329,6 +335,7 @@ class Color(NamedTuple):
|
||||
# Color constants
|
||||
WHITE = Color(255, 255, 255)
|
||||
BLACK = Color(0, 0, 0)
|
||||
TRANSPARENT = Color(0, 0, 0, 0)
|
||||
|
||||
|
||||
class ColorPair(NamedTuple):
|
||||
|
||||
@@ -31,7 +31,7 @@ from .transition import Transition
|
||||
from ..geometry import Spacing, SpacingDimensions, clamp
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..layout import Layout
|
||||
from .._layout import Layout
|
||||
from .styles import DockGroup, Styles, StylesBase
|
||||
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ else:
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..dom import DOMNode
|
||||
from ..layout import Layout
|
||||
from .._layout import Layout
|
||||
|
||||
|
||||
class RulesMap(TypedDict, total=False):
|
||||
@@ -173,7 +173,7 @@ class StylesBase(ABC):
|
||||
layout = LayoutProperty()
|
||||
|
||||
color = ColorProperty(Color(255, 255, 255))
|
||||
background = ColorProperty(Color(0, 0, 0))
|
||||
background = ColorProperty(Color(0, 0, 0, 0))
|
||||
text_style = StyleFlagsProperty()
|
||||
|
||||
opacity = FractionalProperty()
|
||||
|
||||
@@ -10,6 +10,7 @@ from rich.text import Text
|
||||
from rich.tree import Tree
|
||||
|
||||
from ._node_list import NodeList
|
||||
from .color import Color
|
||||
from .css._error_tools import friendly_list
|
||||
from .css.constants import VALID_DISPLAY, VALID_VISIBILITY
|
||||
from .css.errors import StyleValueError
|
||||
@@ -73,18 +74,12 @@ class DOMNode(MessagePump):
|
||||
yield "classes", " ".join(self._classes)
|
||||
|
||||
@property
|
||||
def parent(self) -> DOMNode:
|
||||
def parent(self) -> DOMNode | None:
|
||||
"""Get the parent node.
|
||||
|
||||
Raises:
|
||||
NoParent: If this is the root node.
|
||||
|
||||
Returns:
|
||||
DOMNode: The node which is the direct parent of this node.
|
||||
"""
|
||||
if self._parent is None:
|
||||
raise NoParent(f"{self} has no parent")
|
||||
assert isinstance(self._parent, DOMNode)
|
||||
return self._parent
|
||||
|
||||
@property
|
||||
@@ -231,19 +226,6 @@ class DOMNode(MessagePump):
|
||||
f"expected {friendly_list(VALID_VISIBILITY)})"
|
||||
)
|
||||
|
||||
@property
|
||||
def rich_text_style(self) -> Style:
|
||||
"""Get the text style (added to parent style).
|
||||
|
||||
Returns:
|
||||
Style: Rich Style object.
|
||||
"""
|
||||
return (
|
||||
self.parent.rich_text_style + self.styles.rich_style
|
||||
if self.has_parent
|
||||
else self.styles.rich_style
|
||||
)
|
||||
|
||||
@property
|
||||
def tree(self) -> Tree:
|
||||
"""Get a Rich tree object which will recursively render the structure of the node tree.
|
||||
@@ -287,6 +269,48 @@ class DOMNode(MessagePump):
|
||||
add_children(tree, self)
|
||||
return tree
|
||||
|
||||
@property
|
||||
def rich_text_style(self) -> Style:
|
||||
"""Get the text style object.
|
||||
|
||||
A widget's style is influenced by its parent. For instance if a widgets background has an alpha,
|
||||
then its parent's background color will show throw. Additionally, widgets will inherit their
|
||||
parent's text style (i.e. bold, italic etc).
|
||||
|
||||
Returns:
|
||||
Style: Rich Style object.
|
||||
"""
|
||||
|
||||
# TODO: Feels like there may be opportunity for caching here.
|
||||
|
||||
background = Color(0, 0, 0, 0)
|
||||
color = Color(255, 255, 255, 0)
|
||||
style = Style()
|
||||
for node in reversed(self.ancestors):
|
||||
styles = node.styles
|
||||
if styles.has_rule("background"):
|
||||
background += styles.background
|
||||
if styles.has_rule("color"):
|
||||
color = styles.color
|
||||
style += styles.text_style
|
||||
|
||||
style = Style(bgcolor=background.rich_color, color=color.rich_color) + style
|
||||
return style
|
||||
|
||||
@property
|
||||
def ancestors(self) -> list[DOMNode]:
|
||||
"""Get a list of Nodes by tracing ancestors all the way back to App."""
|
||||
|
||||
nodes: list[DOMNode] = [self]
|
||||
add_node = nodes.append
|
||||
node = self
|
||||
while True:
|
||||
node = node.parent
|
||||
if node is None:
|
||||
break
|
||||
add_node(node)
|
||||
return nodes
|
||||
|
||||
def get_pseudo_classes(self) -> Iterable[str]:
|
||||
"""Get any pseudo classes applicable to this Node, e.g. hover, focus.
|
||||
|
||||
|
||||
@@ -1,41 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import ClassVar, NamedTuple, TYPE_CHECKING
|
||||
from .widget import Widget
|
||||
|
||||
|
||||
from .geometry import Region, Offset, Size
|
||||
class Vertical(Widget):
|
||||
CSS = """
|
||||
Vertical {
|
||||
layout: vertical;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .widget import Widget
|
||||
from .screen import Screen
|
||||
|
||||
|
||||
class WidgetPlacement(NamedTuple):
|
||||
"""The position, size, and relative order of a widget within its parent."""
|
||||
|
||||
region: Region
|
||||
widget: Widget | None = None # A widget of None means empty space
|
||||
order: int = 0
|
||||
|
||||
|
||||
class Layout(ABC):
|
||||
"""Responsible for arranging Widgets in a view and rendering them."""
|
||||
|
||||
name: ClassVar[str] = ""
|
||||
|
||||
@abstractmethod
|
||||
def arrange(
|
||||
self, parent: Widget, size: Size, scroll: Offset
|
||||
) -> tuple[list[WidgetPlacement], set[Widget]]:
|
||||
"""Generate a layout map that defines where on the screen the widgets will be drawn.
|
||||
|
||||
Args:
|
||||
parent (Widget): Parent widget.
|
||||
size (Size): Size of container.
|
||||
scroll (Offset): Offset to apply to the Widget placements.
|
||||
|
||||
Returns:
|
||||
Iterable[WidgetPlacement]: An iterable of widget location
|
||||
"""
|
||||
class Horizontal(Widget):
|
||||
CSS = """
|
||||
Horizontal {
|
||||
layout: horizontal;
|
||||
}
|
||||
"""
|
||||
|
||||
@@ -9,7 +9,7 @@ from typing import Iterable, TYPE_CHECKING, NamedTuple, Sequence
|
||||
from .._layout_resolve import layout_resolve
|
||||
from ..css.types import Edge
|
||||
from ..geometry import Offset, Region, Size
|
||||
from ..layout import Layout, WidgetPlacement
|
||||
from .._layout import Layout, WidgetPlacement
|
||||
from ..widget import Widget
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import sys
|
||||
|
||||
from .horizontal import HorizontalLayout
|
||||
from ..layout import Layout
|
||||
from .._layout import Layout
|
||||
from ..layouts.dock import DockLayout
|
||||
from ..layouts.grid import GridLayout
|
||||
from ..layouts.vertical import VerticalLayout
|
||||
|
||||
@@ -10,7 +10,7 @@ from typing import Iterable, NamedTuple, TYPE_CHECKING
|
||||
|
||||
from .._layout_resolve import layout_resolve
|
||||
from ..geometry import Size, Offset, Region
|
||||
from ..layout import Layout, WidgetPlacement
|
||||
from .._layout import Layout, WidgetPlacement
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..widget import Widget
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from typing import cast
|
||||
|
||||
from textual.geometry import Size, Offset, Region
|
||||
from textual.layout import Layout, WidgetPlacement
|
||||
from textual._layout import Layout, WidgetPlacement
|
||||
|
||||
from textual.widget import Widget
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import cast, TYPE_CHECKING
|
||||
from .. import log
|
||||
|
||||
from ..geometry import Offset, Region, Size
|
||||
from ..layout import Layout, WidgetPlacement
|
||||
from .._layout import Layout, WidgetPlacement
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..widget import Widget
|
||||
|
||||
@@ -11,7 +11,6 @@ from .geometry import Offset, Region
|
||||
from ._compositor import Compositor
|
||||
from .reactive import Reactive
|
||||
from .widget import Widget
|
||||
from .renderables.gradient import VerticalGradient
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
@@ -21,9 +20,10 @@ class Screen(Widget):
|
||||
CSS = """
|
||||
|
||||
Screen {
|
||||
layout: vertical;
|
||||
layout: dock;
|
||||
docks: _default=top;
|
||||
background: $surface;
|
||||
color: $text-surface;
|
||||
}
|
||||
|
||||
"""
|
||||
@@ -38,12 +38,8 @@ class Screen(Widget):
|
||||
def watch_dark(self, dark: bool) -> None:
|
||||
pass
|
||||
|
||||
@property
|
||||
def is_transparent(self) -> bool:
|
||||
return False
|
||||
|
||||
# def render(self) -> RenderableType:
|
||||
# return VerticalGradient("red", "blue")
|
||||
def render(self) -> RenderableType:
|
||||
return self.app.render()
|
||||
|
||||
def get_offset(self, widget: Widget) -> Offset:
|
||||
"""Get the absolute offset of a given Widget.
|
||||
|
||||
@@ -33,7 +33,7 @@ from .dom import DOMNode
|
||||
from .geometry import clamp, Offset, Region, Size, Spacing
|
||||
from .message import Message
|
||||
from . import messages
|
||||
from .layout import Layout
|
||||
from ._layout import Layout
|
||||
from .reactive import Reactive, watch
|
||||
from .renderables.opacity import Opacity
|
||||
|
||||
@@ -111,8 +111,16 @@ class Widget(DOMNode):
|
||||
show_vertical_scrollbar = Reactive(False, layout=True)
|
||||
show_horizontal_scrollbar = Reactive(False, layout=True)
|
||||
|
||||
def mount(self, *anon_widgets: Widget, **widgets: Widget) -> None:
|
||||
self.app.register(self, *anon_widgets, **widgets)
|
||||
self.screen.refresh()
|
||||
|
||||
def compose(self) -> Iterable[Widget]:
|
||||
return
|
||||
yield
|
||||
|
||||
def on_register(self, app: App) -> None:
|
||||
self.app.stylesheet.parse(self.CSS, path=f"<{self.__class__.name}>")
|
||||
self.app.stylesheet.parse(self.CSS, path=f"<{self.__class__.__name__}>")
|
||||
|
||||
def get_box_model(self, container: Size, viewport: Size) -> BoxModel:
|
||||
"""Process the box model for this widget.
|
||||
@@ -246,6 +254,18 @@ class Widget(DOMNode):
|
||||
enabled = self.show_vertical_scrollbar, self.show_horizontal_scrollbar
|
||||
return enabled
|
||||
|
||||
@property
|
||||
def background_color(self) -> Color:
|
||||
color = self.styles.background
|
||||
colors: list[Color] = [color]
|
||||
add_color = colors.append
|
||||
node = self
|
||||
while color.a < 1 and node.parent is not None:
|
||||
node = node.parent
|
||||
color = node.styles.background
|
||||
add_color(color)
|
||||
return sum(reversed(colors), start=Color(0, 0, 0, 0))
|
||||
|
||||
def set_dirty(self) -> None:
|
||||
"""Set the Widget as 'dirty' (requiring re-render)."""
|
||||
self._dirty_regions.clear()
|
||||
@@ -491,8 +511,7 @@ class Widget(DOMNode):
|
||||
Returns:
|
||||
bool: ``True`` if there is background color, otherwise ``False``.
|
||||
"""
|
||||
return False
|
||||
return self.layout is not None
|
||||
return self.is_container and self.styles.background.is_transparent
|
||||
|
||||
@property
|
||||
def console(self) -> Console:
|
||||
@@ -631,8 +650,7 @@ class Widget(DOMNode):
|
||||
if self.is_container:
|
||||
return ""
|
||||
|
||||
label = self.css_identifier_styled
|
||||
return Align.center(label, vertical="middle")
|
||||
return self.css_identifier_styled
|
||||
|
||||
async def action(self, action: str, *params) -> None:
|
||||
await self.app.action(action, self)
|
||||
@@ -693,6 +711,12 @@ class Widget(DOMNode):
|
||||
async def on_key(self, event: events.Key) -> None:
|
||||
await self.dispatch_key(event)
|
||||
|
||||
def on_mount(self, event: events.Mount) -> None:
|
||||
widgets = list(self.compose())
|
||||
if widgets:
|
||||
self.mount(*widgets)
|
||||
self.screen.refresh()
|
||||
|
||||
def on_leave(self) -> None:
|
||||
self.mouse_over = False
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import cast
|
||||
|
||||
from rich.console import RenderableType
|
||||
from rich.text import Text
|
||||
|
||||
@@ -30,14 +32,15 @@ class Button(Widget, can_focus=True):
|
||||
Button:hover {
|
||||
background:$primary-darken-2;
|
||||
color: $text-primary-darken-2;
|
||||
border: tall $primary-lighten-1;
|
||||
|
||||
border: tall $primary-lighten-1;
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
class Pressed(Message, bubble=True):
|
||||
pass
|
||||
@property
|
||||
def button(self) -> Button:
|
||||
return cast(Button, self.sender)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
Reference in New Issue
Block a user