Various changes/fixes

This commit is contained in:
Darren Burns
2022-04-20 13:54:34 +01:00
parent 00112ef740
commit b9d00f301b
7 changed files with 106 additions and 31 deletions

View File

@@ -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;
} }

View File

@@ -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)

View File

@@ -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.

View File

@@ -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:

View File

@@ -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):

View File

@@ -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)

View File

@@ -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