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;
|
layout: vertical;
|
||||||
|
|
||||||
background: dark_green;
|
background: dark_green;
|
||||||
overflow: hidden auto;
|
overflow: auto auto;
|
||||||
border: heavy white;
|
border: heavy white;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10,4 +10,5 @@
|
|||||||
height: 8;
|
height: 8;
|
||||||
min-width: 80;
|
min-width: 80;
|
||||||
background: dark_blue;
|
background: dark_blue;
|
||||||
|
margin-bottom: 4;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import random
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from textual import events
|
from textual import events
|
||||||
from textual.app import App
|
from textual.app import App
|
||||||
|
from textual.geometry import Spacing
|
||||||
from textual.widget import Widget
|
from textual.widget import Widget
|
||||||
from textual.widgets import Placeholder
|
from textual.widgets import Placeholder
|
||||||
|
|
||||||
@@ -14,8 +16,13 @@ class BasicApp(App):
|
|||||||
self.bind("d", "dump")
|
self.bind("d", "dump")
|
||||||
self.bind("t", "log_tree")
|
self.bind("t", "log_tree")
|
||||||
self.bind("p", "print")
|
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."""
|
"""Build layout here."""
|
||||||
|
|
||||||
uber2 = Widget()
|
uber2 = Widget()
|
||||||
@@ -23,8 +30,9 @@ class BasicApp(App):
|
|||||||
Widget(id="uber2-child1"),
|
Widget(id="uber2-child1"),
|
||||||
Widget(id="uber2-child2"),
|
Widget(id="uber2-child2"),
|
||||||
)
|
)
|
||||||
|
self.first_child = Placeholder(id="child1", classes={"list-item"})
|
||||||
uber1 = Widget(
|
uber1 = Widget(
|
||||||
Placeholder(id="child1", classes={"list-item"}),
|
self.first_child,
|
||||||
Placeholder(id="child2", classes={"list-item"}),
|
Placeholder(id="child2", classes={"list-item"}),
|
||||||
Placeholder(id="child3", classes={"list-item"}),
|
Placeholder(id="child3", classes={"list-item"}),
|
||||||
Placeholder(classes={"list-item"}),
|
Placeholder(classes={"list-item"}),
|
||||||
@@ -32,6 +40,7 @@ class BasicApp(App):
|
|||||||
Placeholder(classes={"list-item"}),
|
Placeholder(classes={"list-item"}),
|
||||||
)
|
)
|
||||||
self.mount(uber1=uber1)
|
self.mount(uber1=uber1)
|
||||||
|
await self.first_child.focus()
|
||||||
|
|
||||||
async def on_key(self, event: events.Key) -> None:
|
async def on_key(self, event: events.Key) -> None:
|
||||||
await self.dispatch_key(event)
|
await self.dispatch_key(event)
|
||||||
@@ -55,5 +64,31 @@ class BasicApp(App):
|
|||||||
|
|
||||||
sys.stdout.write("abcdef")
|
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)
|
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))
|
await widget.post_message(events.MouseCapture(self, self.mouse_position))
|
||||||
|
|
||||||
def panic(self, *renderables: RenderableType) -> None:
|
def panic(self, *renderables: RenderableType) -> None:
|
||||||
"""Exits the app after displaying a message.
|
"""Exits the app then displays a message.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
*renderables (RenderableType, optional): Rich renderables to display on exit.
|
*renderables (RenderableType, optional): Rich renderables to display on exit.
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ from typing import Iterable, NamedTuple, TYPE_CHECKING, cast
|
|||||||
import rich.repr
|
import rich.repr
|
||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
|
|
||||||
from .. import log
|
|
||||||
from ..color import Color, ColorPair
|
from ..color import Color, ColorPair
|
||||||
from ._error_tools import friendly_list
|
from ._error_tools import friendly_list
|
||||||
from .constants import NULL_SPACING
|
from .constants import NULL_SPACING
|
||||||
@@ -34,10 +33,8 @@ if TYPE_CHECKING:
|
|||||||
from ..layout import Layout
|
from ..layout import Layout
|
||||||
from .styles import DockGroup, Styles, StylesBase
|
from .styles import DockGroup, Styles, StylesBase
|
||||||
|
|
||||||
|
|
||||||
from .types import EdgeType
|
from .types import EdgeType
|
||||||
|
|
||||||
|
|
||||||
BorderDefinition = (
|
BorderDefinition = (
|
||||||
"Sequence[tuple[EdgeType, str | Color] | None] | tuple[EdgeType, str | Color]"
|
"Sequence[tuple[EdgeType, str | Color] | None] | tuple[EdgeType, str | Color]"
|
||||||
)
|
)
|
||||||
@@ -71,13 +68,15 @@ class ScalarProperty:
|
|||||||
value = obj.get_rule(self.name)
|
value = obj.get_rule(self.name)
|
||||||
return value
|
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
|
"""Set the scalar property
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
obj (Styles): The ``Styles`` object.
|
obj (Styles): The ``Styles`` object.
|
||||||
value (float | Scalar | str | None): The value to set the scalar property to.
|
value (float | int | Scalar | str | None): The value to set the scalar property to.
|
||||||
You can directly pass a float value, which will be interpreted with
|
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%"``,
|
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,
|
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
|
Cells will be used as the unit. Alternatively, you can directly supply
|
||||||
@@ -89,8 +88,9 @@ class ScalarProperty:
|
|||||||
"""
|
"""
|
||||||
if value is None:
|
if value is None:
|
||||||
obj.clear_rule(self.name)
|
obj.clear_rule(self.name)
|
||||||
|
obj.refresh(layout=True)
|
||||||
return
|
return
|
||||||
if isinstance(value, float):
|
if isinstance(value, float) or isinstance(value, int):
|
||||||
new_value = Scalar(float(value), Unit.CELLS, Unit.WIDTH)
|
new_value = Scalar(float(value), Unit.CELLS, Unit.WIDTH)
|
||||||
elif isinstance(value, Scalar):
|
elif isinstance(value, Scalar):
|
||||||
new_value = value
|
new_value = value
|
||||||
@@ -100,7 +100,7 @@ class ScalarProperty:
|
|||||||
except ScalarParseError:
|
except ScalarParseError:
|
||||||
raise StyleValueError("unable to parse scalar from {value!r}")
|
raise StyleValueError("unable to parse scalar from {value!r}")
|
||||||
else:
|
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:
|
if new_value is not None and new_value.unit not in self.units:
|
||||||
raise StyleValueError(
|
raise StyleValueError(
|
||||||
f"{self.name} units must be one of {friendly_list(get_symbols(self.units))}"
|
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:
|
if new_value is not None and new_value.is_percent:
|
||||||
new_value = Scalar(float(new_value.value), self.percent_unit, Unit.WIDTH)
|
new_value = Scalar(float(new_value.value), self.percent_unit, Unit.WIDTH)
|
||||||
if obj.set_rule(self.name, new_value):
|
if obj.set_rule(self.name, new_value):
|
||||||
obj.refresh()
|
obj.refresh(layout=True)
|
||||||
|
|
||||||
|
|
||||||
class BoxProperty:
|
class BoxProperty:
|
||||||
@@ -208,7 +208,15 @@ class Edges(NamedTuple):
|
|||||||
|
|
||||||
|
|
||||||
class BorderProperty:
|
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:
|
def __set_name__(self, owner: StylesBase, name: str) -> None:
|
||||||
self.name = name
|
self.name = name
|
||||||
@@ -262,20 +270,22 @@ class BorderProperty:
|
|||||||
StyleValueError: When the supplied ``tuple`` is not of valid length (1, 2, or 4).
|
StyleValueError: When the supplied ``tuple`` is not of valid length (1, 2, or 4).
|
||||||
"""
|
"""
|
||||||
top, right, bottom, left = self._properties
|
top, right, bottom, left = self._properties
|
||||||
obj.refresh()
|
|
||||||
if border is None:
|
if border is None:
|
||||||
clear_rule = obj.clear_rule
|
clear_rule = obj.clear_rule
|
||||||
clear_rule(top)
|
clear_rule(top)
|
||||||
clear_rule(right)
|
clear_rule(right)
|
||||||
clear_rule(bottom)
|
clear_rule(bottom)
|
||||||
clear_rule(left)
|
clear_rule(left)
|
||||||
|
obj.refresh(layout=self._layout)
|
||||||
return
|
return
|
||||||
if isinstance(border, tuple):
|
if isinstance(border, tuple):
|
||||||
setattr(obj, top, border)
|
setattr(obj, top, border)
|
||||||
setattr(obj, right, border)
|
setattr(obj, right, border)
|
||||||
setattr(obj, bottom, border)
|
setattr(obj, bottom, border)
|
||||||
setattr(obj, left, border)
|
setattr(obj, left, border)
|
||||||
|
obj.refresh(layout=self._layout)
|
||||||
return
|
return
|
||||||
|
|
||||||
count = len(border)
|
count = len(border)
|
||||||
if count == 1:
|
if count == 1:
|
||||||
_border = border[0]
|
_border = border[0]
|
||||||
@@ -286,8 +296,8 @@ class BorderProperty:
|
|||||||
elif count == 2:
|
elif count == 2:
|
||||||
_border1, _border2 = border
|
_border1, _border2 = border
|
||||||
setattr(obj, top, _border1)
|
setattr(obj, top, _border1)
|
||||||
setattr(obj, right, _border1)
|
setattr(obj, bottom, _border1)
|
||||||
setattr(obj, bottom, _border2)
|
setattr(obj, right, _border2)
|
||||||
setattr(obj, left, _border2)
|
setattr(obj, left, _border2)
|
||||||
elif count == 4:
|
elif count == 4:
|
||||||
_border1, _border2, _border3, _border4 = border
|
_border1, _border2, _border3, _border4 = border
|
||||||
@@ -297,6 +307,7 @@ class BorderProperty:
|
|||||||
setattr(obj, left, _border4)
|
setattr(obj, left, _border4)
|
||||||
else:
|
else:
|
||||||
raise StyleValueError("expected 1, 2, or 4 values")
|
raise StyleValueError("expected 1, 2, or 4 values")
|
||||||
|
obj.refresh(layout=self._layout)
|
||||||
|
|
||||||
|
|
||||||
class StyleProperty:
|
class StyleProperty:
|
||||||
@@ -537,9 +548,10 @@ class StringEnumProperty:
|
|||||||
value belongs in the set of valid values.
|
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._valid_values = valid_values
|
||||||
self._default = default
|
self._default = default
|
||||||
|
self._layout = layout
|
||||||
|
|
||||||
def __set_name__(self, owner: StylesBase, name: str) -> None:
|
def __set_name__(self, owner: StylesBase, name: str) -> None:
|
||||||
self.name = name
|
self.name = name
|
||||||
@@ -569,14 +581,14 @@ class StringEnumProperty:
|
|||||||
|
|
||||||
if value is None:
|
if value is None:
|
||||||
if obj.clear_rule(self.name):
|
if obj.clear_rule(self.name):
|
||||||
obj.refresh()
|
obj.refresh(layout=self._layout)
|
||||||
else:
|
else:
|
||||||
if value not in self._valid_values:
|
if value not in self._valid_values:
|
||||||
raise StyleValueError(
|
raise StyleValueError(
|
||||||
f"{self.name} must be one of {friendly_list(self._valid_values)}"
|
f"{self.name} must be one of {friendly_list(self._valid_values)}"
|
||||||
)
|
)
|
||||||
if obj.set_rule(self.name, value):
|
if obj.set_rule(self.name, value):
|
||||||
obj.refresh()
|
obj.refresh(layout=self._layout)
|
||||||
|
|
||||||
|
|
||||||
class NameProperty:
|
class NameProperty:
|
||||||
|
|||||||
@@ -156,6 +156,25 @@ class Scalar(NamedTuple):
|
|||||||
raise ScalarResolveError(f"expected dimensions; found {str(self)!r}")
|
raise ScalarResolveError(f"expected dimensions; found {str(self)!r}")
|
||||||
return dimension
|
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)
|
@rich.repr.auto(angular=True)
|
||||||
class ScalarOffset(NamedTuple):
|
class ScalarOffset(NamedTuple):
|
||||||
|
|||||||
@@ -59,10 +59,9 @@ if TYPE_CHECKING:
|
|||||||
class RulesMap(TypedDict, total=False):
|
class RulesMap(TypedDict, total=False):
|
||||||
"""A typed dict for CSS rules.
|
"""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.
|
Does not define composite rules, that is a rule that is made of a combination of other rules.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
display: Display
|
display: Display
|
||||||
@@ -150,7 +149,7 @@ class StylesBase(ABC):
|
|||||||
"scrollbar_background_active",
|
"scrollbar_background_active",
|
||||||
}
|
}
|
||||||
|
|
||||||
display = StringEnumProperty(VALID_DISPLAY, "block")
|
display = StringEnumProperty(VALID_DISPLAY, "block", layout=True)
|
||||||
visibility = StringEnumProperty(VALID_VISIBILITY, "visible")
|
visibility = StringEnumProperty(VALID_VISIBILITY, "visible")
|
||||||
layout = LayoutProperty()
|
layout = LayoutProperty()
|
||||||
|
|
||||||
@@ -164,19 +163,19 @@ class StylesBase(ABC):
|
|||||||
margin = SpacingProperty()
|
margin = SpacingProperty()
|
||||||
offset = OffsetProperty()
|
offset = OffsetProperty()
|
||||||
|
|
||||||
border = BorderProperty()
|
border = BorderProperty(layout=True)
|
||||||
border_top = BoxProperty(Color(0, 255, 0))
|
border_top = BoxProperty(Color(0, 255, 0))
|
||||||
border_right = BoxProperty(Color(0, 255, 0))
|
border_right = BoxProperty(Color(0, 255, 0))
|
||||||
border_bottom = BoxProperty(Color(0, 255, 0))
|
border_bottom = BoxProperty(Color(0, 255, 0))
|
||||||
border_left = 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_top = BoxProperty(Color(0, 255, 0))
|
||||||
outline_right = BoxProperty(Color(0, 255, 0))
|
outline_right = BoxProperty(Color(0, 255, 0))
|
||||||
outline_bottom = BoxProperty(Color(0, 255, 0))
|
outline_bottom = BoxProperty(Color(0, 255, 0))
|
||||||
outline_left = 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)
|
width = ScalarProperty(percent_unit=Unit.WIDTH)
|
||||||
height = ScalarProperty(percent_unit=Unit.HEIGHT)
|
height = ScalarProperty(percent_unit=Unit.HEIGHT)
|
||||||
min_width = ScalarProperty(percent_unit=Unit.WIDTH)
|
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 __future__ import annotations
|
||||||
|
|
||||||
from math import sqrt
|
from math import sqrt
|
||||||
from typing import Any, cast, NamedTuple, Tuple, Union, TypeVar
|
from typing import Any, cast, NamedTuple, Tuple, Union, TypeVar
|
||||||
|
|
||||||
|
|
||||||
SpacingDimensions = Union[int, Tuple[int], Tuple[int, int], Tuple[int, int, int, int]]
|
SpacingDimensions = Union[int, Tuple[int], Tuple[int, int], Tuple[int, int, int, int]]
|
||||||
|
|
||||||
|
|
||||||
T = TypeVar("T", int, float)
|
T = TypeVar("T", int, float)
|
||||||
|
|
||||||
|
|
||||||
@@ -632,11 +629,11 @@ class Spacing(NamedTuple):
|
|||||||
def unpack(cls, pad: SpacingDimensions) -> Spacing:
|
def unpack(cls, pad: SpacingDimensions) -> Spacing:
|
||||||
"""Unpack padding specified in CSS style."""
|
"""Unpack padding specified in CSS style."""
|
||||||
if isinstance(pad, int):
|
if isinstance(pad, int):
|
||||||
return cls(pad, pad, pad, pad)
|
return Spacing.all(pad)
|
||||||
pad_len = len(pad)
|
pad_len = len(pad)
|
||||||
if pad_len == 1:
|
if pad_len == 1:
|
||||||
_pad = pad[0]
|
_pad = pad[0]
|
||||||
return cls(_pad, _pad, _pad, _pad)
|
return Spacing.all(_pad)
|
||||||
if pad_len == 2:
|
if pad_len == 2:
|
||||||
pad_top, pad_right = cast(Tuple[int, int], pad)
|
pad_top, pad_right = cast(Tuple[int, int], pad)
|
||||||
return cls(pad_top, pad_right, pad_top, pad_right)
|
return cls(pad_top, pad_right, pad_top, pad_right)
|
||||||
@@ -645,6 +642,18 @@ class Spacing(NamedTuple):
|
|||||||
return cls(top, right, bottom, left)
|
return cls(top, right, bottom, left)
|
||||||
raise ValueError(f"1, 2 or 4 integers required for spacing; {pad_len} given")
|
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:
|
def __add__(self, other: object) -> Spacing:
|
||||||
if isinstance(other, tuple):
|
if isinstance(other, tuple):
|
||||||
top1, right1, bottom1, left1 = self
|
top1, right1, bottom1, left1 = self
|
||||||
|
|||||||
Reference in New Issue
Block a user