mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Various changes/fixes
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
layout: vertical;
|
||||
|
||||
background: dark_green;
|
||||
overflow: hidden auto;
|
||||
overflow: auto auto;
|
||||
border: heavy white;
|
||||
}
|
||||
|
||||
@@ -10,4 +10,5 @@
|
||||
height: 8;
|
||||
min-width: 80;
|
||||
background: dark_blue;
|
||||
margin-bottom: 4;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import random
|
||||
import sys
|
||||
|
||||
from textual import events
|
||||
from textual.app import App
|
||||
from textual.geometry import Spacing
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Placeholder
|
||||
|
||||
@@ -14,8 +16,13 @@ class BasicApp(App):
|
||||
self.bind("d", "dump")
|
||||
self.bind("t", "log_tree")
|
||||
self.bind("p", "print")
|
||||
self.bind("o", "toggle_visibility")
|
||||
self.bind("p", "toggle_display")
|
||||
self.bind("f", "modify_first_child")
|
||||
self.bind("b", "toggle_border")
|
||||
self.bind("m", "increase_margin")
|
||||
|
||||
def on_mount(self):
|
||||
async def on_mount(self):
|
||||
"""Build layout here."""
|
||||
|
||||
uber2 = Widget()
|
||||
@@ -23,8 +30,9 @@ class BasicApp(App):
|
||||
Widget(id="uber2-child1"),
|
||||
Widget(id="uber2-child2"),
|
||||
)
|
||||
self.first_child = Placeholder(id="child1", classes={"list-item"})
|
||||
uber1 = Widget(
|
||||
Placeholder(id="child1", classes={"list-item"}),
|
||||
self.first_child,
|
||||
Placeholder(id="child2", classes={"list-item"}),
|
||||
Placeholder(id="child3", classes={"list-item"}),
|
||||
Placeholder(classes={"list-item"}),
|
||||
@@ -32,6 +40,7 @@ class BasicApp(App):
|
||||
Placeholder(classes={"list-item"}),
|
||||
)
|
||||
self.mount(uber1=uber1)
|
||||
await self.first_child.focus()
|
||||
|
||||
async def on_key(self, event: events.Key) -> None:
|
||||
await self.dispatch_key(event)
|
||||
@@ -55,5 +64,31 @@ class BasicApp(App):
|
||||
|
||||
sys.stdout.write("abcdef")
|
||||
|
||||
def action_modify_first_child(self):
|
||||
"""Increment height of first child widget, randomise border and bg color"""
|
||||
previous_height = self.focused.styles.height.value
|
||||
new_height = previous_height + 1
|
||||
self.focused.styles.height = self.focused.styles.height.copy_with(
|
||||
value=new_height
|
||||
)
|
||||
color = random.choice(["red", "green", "blue"])
|
||||
self.focused.styles.background = color
|
||||
self.focused.styles.border = ("dashed", color)
|
||||
|
||||
def action_toggle_visibility(self):
|
||||
self.focused.visible = not self.focused.visible
|
||||
|
||||
def action_toggle_display(self):
|
||||
# TODO: Doesn't work
|
||||
self.focused.display = not self.focused.display
|
||||
|
||||
def action_toggle_border(self):
|
||||
self.focused.styles.border = [("solid", "red"), ("solid", "white")]
|
||||
|
||||
def action_increase_margin(self):
|
||||
old_margin = self.focused.styles.margin
|
||||
new_margin = old_margin + Spacing.all(1)
|
||||
self.focused.styles.margin = new_margin
|
||||
|
||||
|
||||
BasicApp.run(css_file="uber.css", log="textual.log", log_verbosity=1)
|
||||
|
||||
@@ -439,7 +439,7 @@ class App(DOMNode):
|
||||
await widget.post_message(events.MouseCapture(self, self.mouse_position))
|
||||
|
||||
def panic(self, *renderables: RenderableType) -> None:
|
||||
"""Exits the app after displaying a message.
|
||||
"""Exits the app then displays a message.
|
||||
|
||||
Args:
|
||||
*renderables (RenderableType, optional): Rich renderables to display on exit.
|
||||
|
||||
@@ -14,7 +14,6 @@ from typing import Iterable, NamedTuple, TYPE_CHECKING, cast
|
||||
import rich.repr
|
||||
from rich.style import Style
|
||||
|
||||
from .. import log
|
||||
from ..color import Color, ColorPair
|
||||
from ._error_tools import friendly_list
|
||||
from .constants import NULL_SPACING
|
||||
@@ -34,10 +33,8 @@ if TYPE_CHECKING:
|
||||
from ..layout import Layout
|
||||
from .styles import DockGroup, Styles, StylesBase
|
||||
|
||||
|
||||
from .types import EdgeType
|
||||
|
||||
|
||||
BorderDefinition = (
|
||||
"Sequence[tuple[EdgeType, str | Color] | None] | tuple[EdgeType, str | Color]"
|
||||
)
|
||||
@@ -71,13 +68,15 @@ class ScalarProperty:
|
||||
value = obj.get_rule(self.name)
|
||||
return value
|
||||
|
||||
def __set__(self, obj: StylesBase, value: float | Scalar | str | None) -> None:
|
||||
def __set__(
|
||||
self, obj: StylesBase, value: float | int | Scalar | str | None
|
||||
) -> None:
|
||||
"""Set the scalar property
|
||||
|
||||
Args:
|
||||
obj (Styles): The ``Styles`` object.
|
||||
value (float | Scalar | str | None): The value to set the scalar property to.
|
||||
You can directly pass a float value, which will be interpreted with
|
||||
value (float | int | Scalar | str | None): The value to set the scalar property to.
|
||||
You can directly pass a float or int value, which will be interpreted with
|
||||
a default unit of Cells. You may also provide a string such as ``"50%"``,
|
||||
as you might do when writing CSS. If a string with no units is supplied,
|
||||
Cells will be used as the unit. Alternatively, you can directly supply
|
||||
@@ -89,8 +88,9 @@ class ScalarProperty:
|
||||
"""
|
||||
if value is None:
|
||||
obj.clear_rule(self.name)
|
||||
obj.refresh(layout=True)
|
||||
return
|
||||
if isinstance(value, float):
|
||||
if isinstance(value, float) or isinstance(value, int):
|
||||
new_value = Scalar(float(value), Unit.CELLS, Unit.WIDTH)
|
||||
elif isinstance(value, Scalar):
|
||||
new_value = value
|
||||
@@ -100,7 +100,7 @@ class ScalarProperty:
|
||||
except ScalarParseError:
|
||||
raise StyleValueError("unable to parse scalar from {value!r}")
|
||||
else:
|
||||
raise StyleValueError("expected float, Scalar, or None")
|
||||
raise StyleValueError("expected float, int, Scalar, or None")
|
||||
if new_value is not None and new_value.unit not in self.units:
|
||||
raise StyleValueError(
|
||||
f"{self.name} units must be one of {friendly_list(get_symbols(self.units))}"
|
||||
@@ -108,7 +108,7 @@ class ScalarProperty:
|
||||
if new_value is not None and new_value.is_percent:
|
||||
new_value = Scalar(float(new_value.value), self.percent_unit, Unit.WIDTH)
|
||||
if obj.set_rule(self.name, new_value):
|
||||
obj.refresh()
|
||||
obj.refresh(layout=True)
|
||||
|
||||
|
||||
class BoxProperty:
|
||||
@@ -208,7 +208,15 @@ class Edges(NamedTuple):
|
||||
|
||||
|
||||
class BorderProperty:
|
||||
"""Descriptor for getting and setting full borders and outlines."""
|
||||
"""Descriptor for getting and setting full borders and outlines.
|
||||
|
||||
Args:
|
||||
layout (bool): True if the layout should be refreshed after setting, False otherwise.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, layout: bool) -> None:
|
||||
self._layout = layout
|
||||
|
||||
def __set_name__(self, owner: StylesBase, name: str) -> None:
|
||||
self.name = name
|
||||
@@ -262,20 +270,22 @@ class BorderProperty:
|
||||
StyleValueError: When the supplied ``tuple`` is not of valid length (1, 2, or 4).
|
||||
"""
|
||||
top, right, bottom, left = self._properties
|
||||
obj.refresh()
|
||||
if border is None:
|
||||
clear_rule = obj.clear_rule
|
||||
clear_rule(top)
|
||||
clear_rule(right)
|
||||
clear_rule(bottom)
|
||||
clear_rule(left)
|
||||
obj.refresh(layout=self._layout)
|
||||
return
|
||||
if isinstance(border, tuple):
|
||||
setattr(obj, top, border)
|
||||
setattr(obj, right, border)
|
||||
setattr(obj, bottom, border)
|
||||
setattr(obj, left, border)
|
||||
obj.refresh(layout=self._layout)
|
||||
return
|
||||
|
||||
count = len(border)
|
||||
if count == 1:
|
||||
_border = border[0]
|
||||
@@ -286,8 +296,8 @@ class BorderProperty:
|
||||
elif count == 2:
|
||||
_border1, _border2 = border
|
||||
setattr(obj, top, _border1)
|
||||
setattr(obj, right, _border1)
|
||||
setattr(obj, bottom, _border2)
|
||||
setattr(obj, bottom, _border1)
|
||||
setattr(obj, right, _border2)
|
||||
setattr(obj, left, _border2)
|
||||
elif count == 4:
|
||||
_border1, _border2, _border3, _border4 = border
|
||||
@@ -297,6 +307,7 @@ class BorderProperty:
|
||||
setattr(obj, left, _border4)
|
||||
else:
|
||||
raise StyleValueError("expected 1, 2, or 4 values")
|
||||
obj.refresh(layout=self._layout)
|
||||
|
||||
|
||||
class StyleProperty:
|
||||
@@ -537,9 +548,10 @@ class StringEnumProperty:
|
||||
value belongs in the set of valid values.
|
||||
"""
|
||||
|
||||
def __init__(self, valid_values: set[str], default: str) -> None:
|
||||
def __init__(self, valid_values: set[str], default: str, layout=False) -> None:
|
||||
self._valid_values = valid_values
|
||||
self._default = default
|
||||
self._layout = layout
|
||||
|
||||
def __set_name__(self, owner: StylesBase, name: str) -> None:
|
||||
self.name = name
|
||||
@@ -569,14 +581,14 @@ class StringEnumProperty:
|
||||
|
||||
if value is None:
|
||||
if obj.clear_rule(self.name):
|
||||
obj.refresh()
|
||||
obj.refresh(layout=self._layout)
|
||||
else:
|
||||
if value not in self._valid_values:
|
||||
raise StyleValueError(
|
||||
f"{self.name} must be one of {friendly_list(self._valid_values)}"
|
||||
)
|
||||
if obj.set_rule(self.name, value):
|
||||
obj.refresh()
|
||||
obj.refresh(layout=self._layout)
|
||||
|
||||
|
||||
class NameProperty:
|
||||
|
||||
@@ -156,6 +156,25 @@ class Scalar(NamedTuple):
|
||||
raise ScalarResolveError(f"expected dimensions; found {str(self)!r}")
|
||||
return dimension
|
||||
|
||||
def copy_with(
|
||||
self,
|
||||
value: float | None = None,
|
||||
unit: Unit | None = None,
|
||||
percent_unit: Unit | None = None,
|
||||
) -> Scalar:
|
||||
"""Get a copy of this Scalar, with values optionally modified
|
||||
|
||||
Args:
|
||||
value (float | None): The new value, or None to keep the same value
|
||||
unit (Unit | None): The new unit, or None to keep the same unit
|
||||
percent_unit (Unit | None): The new percent_unit, or None to keep the same unit
|
||||
"""
|
||||
return Scalar(
|
||||
value if value is not None else self.value,
|
||||
unit if unit is not None else self.unit,
|
||||
percent_unit if percent_unit is not None else self.percent_unit,
|
||||
)
|
||||
|
||||
|
||||
@rich.repr.auto(angular=True)
|
||||
class ScalarOffset(NamedTuple):
|
||||
|
||||
@@ -59,10 +59,9 @@ if TYPE_CHECKING:
|
||||
class RulesMap(TypedDict, total=False):
|
||||
"""A typed dict for CSS rules.
|
||||
|
||||
Any key may be absent, indiciating that rule has not been set.
|
||||
Any key may be absent, indicating that rule has not been set.
|
||||
|
||||
Does not define composite rules, that is a rule that is made of a combination of other rules.
|
||||
|
||||
"""
|
||||
|
||||
display: Display
|
||||
@@ -150,7 +149,7 @@ class StylesBase(ABC):
|
||||
"scrollbar_background_active",
|
||||
}
|
||||
|
||||
display = StringEnumProperty(VALID_DISPLAY, "block")
|
||||
display = StringEnumProperty(VALID_DISPLAY, "block", layout=True)
|
||||
visibility = StringEnumProperty(VALID_VISIBILITY, "visible")
|
||||
layout = LayoutProperty()
|
||||
|
||||
@@ -164,19 +163,19 @@ class StylesBase(ABC):
|
||||
margin = SpacingProperty()
|
||||
offset = OffsetProperty()
|
||||
|
||||
border = BorderProperty()
|
||||
border = BorderProperty(layout=True)
|
||||
border_top = BoxProperty(Color(0, 255, 0))
|
||||
border_right = BoxProperty(Color(0, 255, 0))
|
||||
border_bottom = BoxProperty(Color(0, 255, 0))
|
||||
border_left = BoxProperty(Color(0, 255, 0))
|
||||
|
||||
outline = BorderProperty()
|
||||
outline = BorderProperty(layout=False)
|
||||
outline_top = BoxProperty(Color(0, 255, 0))
|
||||
outline_right = BoxProperty(Color(0, 255, 0))
|
||||
outline_bottom = BoxProperty(Color(0, 255, 0))
|
||||
outline_left = BoxProperty(Color(0, 255, 0))
|
||||
|
||||
box_sizing = StringEnumProperty(VALID_BOX_SIZING, "border-box")
|
||||
box_sizing = StringEnumProperty(VALID_BOX_SIZING, "border-box", layout=True)
|
||||
width = ScalarProperty(percent_unit=Unit.WIDTH)
|
||||
height = ScalarProperty(percent_unit=Unit.HEIGHT)
|
||||
min_width = ScalarProperty(percent_unit=Unit.WIDTH)
|
||||
|
||||
@@ -4,16 +4,13 @@ Functions and classes to manage terminal geometry (anything involving coordinate
|
||||
|
||||
"""
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from math import sqrt
|
||||
from typing import Any, cast, NamedTuple, Tuple, Union, TypeVar
|
||||
|
||||
|
||||
SpacingDimensions = Union[int, Tuple[int], Tuple[int, int], Tuple[int, int, int, int]]
|
||||
|
||||
|
||||
T = TypeVar("T", int, float)
|
||||
|
||||
|
||||
@@ -632,11 +629,11 @@ class Spacing(NamedTuple):
|
||||
def unpack(cls, pad: SpacingDimensions) -> Spacing:
|
||||
"""Unpack padding specified in CSS style."""
|
||||
if isinstance(pad, int):
|
||||
return cls(pad, pad, pad, pad)
|
||||
return Spacing.all(pad)
|
||||
pad_len = len(pad)
|
||||
if pad_len == 1:
|
||||
_pad = pad[0]
|
||||
return cls(_pad, _pad, _pad, _pad)
|
||||
return Spacing.all(_pad)
|
||||
if pad_len == 2:
|
||||
pad_top, pad_right = cast(Tuple[int, int], pad)
|
||||
return cls(pad_top, pad_right, pad_top, pad_right)
|
||||
@@ -645,6 +642,18 @@ class Spacing(NamedTuple):
|
||||
return cls(top, right, bottom, left)
|
||||
raise ValueError(f"1, 2 or 4 integers required for spacing; {pad_len} given")
|
||||
|
||||
@classmethod
|
||||
def vertical(cls, amount: int) -> Spacing:
|
||||
return Spacing(amount, 0, amount, 0)
|
||||
|
||||
@classmethod
|
||||
def horizontal(cls, amount: int) -> Spacing:
|
||||
return Spacing(0, amount, 0, amount)
|
||||
|
||||
@classmethod
|
||||
def all(cls, amount: int) -> Spacing:
|
||||
return Spacing(amount, amount, amount, amount)
|
||||
|
||||
def __add__(self, other: object) -> Spacing:
|
||||
if isinstance(other, tuple):
|
||||
top1, right1, bottom1, left1 = self
|
||||
|
||||
Reference in New Issue
Block a user