mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
css align
This commit is contained in:
19
sandbox/align.css
Normal file
19
sandbox/align.css
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
|
||||||
|
Screen {
|
||||||
|
layout: horizontal;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget#thing {
|
||||||
|
width: 20;
|
||||||
|
height: 10;
|
||||||
|
align: center middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Widget#thing2 {
|
||||||
|
width: 30;
|
||||||
|
height: 8;
|
||||||
|
align: center middle;
|
||||||
|
background: green;
|
||||||
|
}
|
||||||
17
sandbox/align.py
Normal file
17
sandbox/align.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from textual.app import App
|
||||||
|
from textual.widget import Widget
|
||||||
|
|
||||||
|
|
||||||
|
class AlignApp(App):
|
||||||
|
def on_load(self):
|
||||||
|
self.bind("t", "log_tree")
|
||||||
|
|
||||||
|
def on_mount(self) -> None:
|
||||||
|
self.log("MOUNTED")
|
||||||
|
self.mount(thing=Widget(), thing2=Widget())
|
||||||
|
|
||||||
|
def action_log_tree(self):
|
||||||
|
self.log(self.screen.tree)
|
||||||
|
|
||||||
|
|
||||||
|
AlignApp.run(css_file="align.css", log="textual.log", watch_css=True)
|
||||||
@@ -459,6 +459,7 @@ class App(DOMNode):
|
|||||||
Args:
|
Args:
|
||||||
error (Exception): An exception instance.
|
error (Exception): An exception instance.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if hasattr(error, "__rich__"):
|
if hasattr(error, "__rich__"):
|
||||||
# Exception has a rich method, so we can defer to that for the rendering
|
# Exception has a rich method, so we can defer to that for the rendering
|
||||||
self.panic(error)
|
self.panic(error)
|
||||||
@@ -489,15 +490,9 @@ class App(DOMNode):
|
|||||||
if os.getenv("TEXTUAL_DEVTOOLS") == "1":
|
if os.getenv("TEXTUAL_DEVTOOLS") == "1":
|
||||||
try:
|
try:
|
||||||
await self.devtools.connect()
|
await self.devtools.connect()
|
||||||
if self._log_console:
|
self.log(f"Connected to devtools ({self.devtools.url})")
|
||||||
self._log_console.print(
|
|
||||||
f"Connected to devtools ({self.devtools.url})"
|
|
||||||
)
|
|
||||||
except DevtoolsConnectionError:
|
except DevtoolsConnectionError:
|
||||||
if self._log_console:
|
self.log(f"Couldn't connect to devtools ({self.devtools.url})")
|
||||||
self._log_console.print(
|
|
||||||
f"Couldn't connect to devtools ({self.devtools.url})"
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
if self.css_file is not None:
|
if self.css_file is not None:
|
||||||
self.stylesheet.read(self.css_file)
|
self.stylesheet.read(self.css_file)
|
||||||
@@ -529,10 +524,11 @@ class App(DOMNode):
|
|||||||
self.title = self._title
|
self.title = self._title
|
||||||
self.refresh()
|
self.refresh()
|
||||||
await self.animator.start()
|
await self.animator.start()
|
||||||
|
|
||||||
with redirect_stdout(StdoutRedirector(self.devtools, self._log_file)): # type: ignore
|
|
||||||
await super().process_messages()
|
await super().process_messages()
|
||||||
log("Message processing stopped")
|
log("PROCESS END")
|
||||||
|
if self.devtools.is_connected:
|
||||||
|
await self._disconnect_devtools()
|
||||||
|
self.log(f"Disconnected from devtools ({self.devtools.url})")
|
||||||
with timer("animator.stop()"):
|
with timer("animator.stop()"):
|
||||||
await self.animator.stop()
|
await self.animator.stop()
|
||||||
with timer("self.close_all()"):
|
with timer("self.close_all()"):
|
||||||
@@ -545,12 +541,6 @@ class App(DOMNode):
|
|||||||
self._running = False
|
self._running = False
|
||||||
if self._exit_renderables:
|
if self._exit_renderables:
|
||||||
self._print_error_renderables()
|
self._print_error_renderables()
|
||||||
if self.devtools.is_connected:
|
|
||||||
await self._disconnect_devtools()
|
|
||||||
if self._log_console is not None:
|
|
||||||
self._log_console.print(
|
|
||||||
f"Disconnected from devtools ({self.devtools.url})"
|
|
||||||
)
|
|
||||||
if self._log_file is not None:
|
if self._log_file is not None:
|
||||||
self._log_file.close()
|
self._log_file.close()
|
||||||
|
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ class Color(NamedTuple):
|
|||||||
@property
|
@property
|
||||||
def rich_color(self) -> RichColor:
|
def rich_color(self) -> RichColor:
|
||||||
"""This color encoded in Rich's Color class."""
|
"""This color encoded in Rich's Color class."""
|
||||||
|
# TODO: This isn't cheap as I'd like - cache in a LRUCache ?
|
||||||
r, g, b, _a = self
|
r, g, b, _a = self
|
||||||
return RichColor.from_rgb(r, g, b)
|
return RichColor.from_rgb(r, g, b)
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import rich.repr
|
|||||||
|
|
||||||
from ._error_tools import friendly_list
|
from ._error_tools import friendly_list
|
||||||
from .constants import (
|
from .constants import (
|
||||||
|
VALID_ALIGN_HORIZONTAL,
|
||||||
|
VALID_ALIGN_VERTICAL,
|
||||||
VALID_BORDER,
|
VALID_BORDER,
|
||||||
VALID_BOX_SIZING,
|
VALID_BOX_SIZING,
|
||||||
VALID_EDGE,
|
VALID_EDGE,
|
||||||
@@ -600,3 +602,30 @@ class StylesBuilder:
|
|||||||
transitions[css_property] = Transition(duration, easing, delay)
|
transitions[css_property] = Transition(duration, easing, delay)
|
||||||
|
|
||||||
self.styles._rules["transitions"] = transitions
|
self.styles._rules["transitions"] = transitions
|
||||||
|
|
||||||
|
def process_align(self, name: str, tokens: list[Token]) -> None:
|
||||||
|
if len(tokens) != 2:
|
||||||
|
self.error(name, tokens[0], "expected two tokens")
|
||||||
|
token_horizontal = tokens[0]
|
||||||
|
token_vertical = tokens[1]
|
||||||
|
if token_horizontal.name != "token":
|
||||||
|
self.error(
|
||||||
|
name,
|
||||||
|
token_horizontal,
|
||||||
|
f"invalid token {token_horizontal!r}, expected {friendly_list(VALID_ALIGN_HORIZONTAL)}",
|
||||||
|
)
|
||||||
|
if token_vertical.name != "token":
|
||||||
|
self.error(
|
||||||
|
name,
|
||||||
|
token_vertical,
|
||||||
|
f"invalid token {token_vertical!r}, expected {friendly_list(VALID_ALIGN_VERTICAL)}",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.styles._rules["align_horizontal"] = token_horizontal.value
|
||||||
|
self.styles._rules["align_vertical"] = token_vertical.value
|
||||||
|
|
||||||
|
def process_align_horizontal(self, name: str, tokens: list[Token]) -> None:
|
||||||
|
self._process_enum(name, tokens, VALID_ALIGN_HORIZONTAL)
|
||||||
|
|
||||||
|
def process_align_vertical(self, name: str, tokens: list[Token]) -> None:
|
||||||
|
self._process_enum(name, tokens, VALID_ALIGN_VERTICAL)
|
||||||
|
|||||||
@@ -29,6 +29,6 @@ VALID_LAYOUT: Final = {"dock", "vertical", "grid"}
|
|||||||
|
|
||||||
VALID_BOX_SIZING: Final = {"border-box", "content-box"}
|
VALID_BOX_SIZING: Final = {"border-box", "content-box"}
|
||||||
VALID_OVERFLOW: Final = {"scroll", "hidden", "auto"}
|
VALID_OVERFLOW: Final = {"scroll", "hidden", "auto"}
|
||||||
|
VALID_ALIGN_HORIZONTAL: Final = {"left", "center", "right"}
|
||||||
|
VALID_ALIGN_VERTICAL: Final = {"top", "middle", "bottom"}
|
||||||
NULL_SPACING: Final = Spacing(0, 0, 0, 0)
|
NULL_SPACING: Final = Spacing(0, 0, 0, 0)
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class Unit(Enum):
|
|||||||
HEIGHT = 5
|
HEIGHT = 5
|
||||||
VIEW_WIDTH = 6
|
VIEW_WIDTH = 6
|
||||||
VIEW_HEIGHT = 7
|
VIEW_HEIGHT = 7
|
||||||
|
AUTO = 8
|
||||||
|
|
||||||
|
|
||||||
UNIT_SYMBOL = {
|
UNIT_SYMBOL = {
|
||||||
@@ -124,6 +125,9 @@ class Scalar(NamedTuple):
|
|||||||
Returns:
|
Returns:
|
||||||
Scalar: New scalar
|
Scalar: New scalar
|
||||||
"""
|
"""
|
||||||
|
if token.lower() == "auto":
|
||||||
|
scalar = cls(1.0, Unit.AUTO, Unit.AUTO)
|
||||||
|
else:
|
||||||
match = _MATCH_SCALAR(token)
|
match = _MATCH_SCALAR(token)
|
||||||
if match is None:
|
if match is None:
|
||||||
raise ScalarParseError(f"{token!r} is not a valid scalar")
|
raise ScalarParseError(f"{token!r} is not a valid scalar")
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from rich.style import Style
|
|||||||
from .. import log
|
from .. import log
|
||||||
from .._animator import Animation, EasingFunction
|
from .._animator import Animation, EasingFunction
|
||||||
from ..color import Color
|
from ..color import Color
|
||||||
from ..geometry import Size, Spacing
|
from ..geometry import Offset, Size, Spacing
|
||||||
from ._style_properties import (
|
from ._style_properties import (
|
||||||
BorderProperty,
|
BorderProperty,
|
||||||
BoxProperty,
|
BoxProperty,
|
||||||
@@ -32,17 +32,28 @@ from ._style_properties import (
|
|||||||
TransitionsProperty,
|
TransitionsProperty,
|
||||||
FractionalProperty,
|
FractionalProperty,
|
||||||
)
|
)
|
||||||
from .constants import VALID_BOX_SIZING, VALID_DISPLAY, VALID_VISIBILITY, VALID_OVERFLOW
|
from .constants import (
|
||||||
|
VALID_ALIGN_HORIZONTAL,
|
||||||
|
VALID_ALIGN_VERTICAL,
|
||||||
|
VALID_BOX_SIZING,
|
||||||
|
VALID_DISPLAY,
|
||||||
|
VALID_VISIBILITY,
|
||||||
|
VALID_OVERFLOW,
|
||||||
|
)
|
||||||
from .scalar import Scalar, ScalarOffset, Unit
|
from .scalar import Scalar, ScalarOffset, Unit
|
||||||
from .scalar_animation import ScalarAnimation
|
from .scalar_animation import ScalarAnimation
|
||||||
from .transition import Transition
|
from .transition import Transition
|
||||||
from .types import (
|
from .types import (
|
||||||
BoxSizing,
|
BoxSizing,
|
||||||
Display,
|
Display,
|
||||||
|
AlignHorizontal,
|
||||||
|
AlignVertical,
|
||||||
Edge,
|
Edge,
|
||||||
|
AlignHorizontal,
|
||||||
Overflow,
|
Overflow,
|
||||||
Specificity3,
|
Specificity3,
|
||||||
Specificity4,
|
Specificity4,
|
||||||
|
AlignVertical,
|
||||||
Visibility,
|
Visibility,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -116,6 +127,9 @@ class RulesMap(TypedDict, total=False):
|
|||||||
scrollbar_background_hover: Color
|
scrollbar_background_hover: Color
|
||||||
scrollbar_background_active: Color
|
scrollbar_background_active: Color
|
||||||
|
|
||||||
|
align_horizontal: AlignHorizontal
|
||||||
|
align_vertical: AlignVertical
|
||||||
|
|
||||||
|
|
||||||
RULE_NAMES = list(RulesMap.__annotations__.keys())
|
RULE_NAMES = list(RulesMap.__annotations__.keys())
|
||||||
_rule_getter = attrgetter(*RULE_NAMES)
|
_rule_getter = attrgetter(*RULE_NAMES)
|
||||||
@@ -204,6 +218,9 @@ class StylesBase(ABC):
|
|||||||
scrollbar_background_hover = ColorProperty("#444444")
|
scrollbar_background_hover = ColorProperty("#444444")
|
||||||
scrollbar_background_active = ColorProperty("black")
|
scrollbar_background_active = ColorProperty("black")
|
||||||
|
|
||||||
|
align_horizontal = StringEnumProperty(VALID_ALIGN_HORIZONTAL, "left")
|
||||||
|
align_vertical = StringEnumProperty(VALID_ALIGN_VERTICAL, "top")
|
||||||
|
|
||||||
def __eq__(self, styles: object) -> bool:
|
def __eq__(self, styles: object) -> bool:
|
||||||
"""Check that Styles containts the same rules."""
|
"""Check that Styles containts the same rules."""
|
||||||
if not isinstance(styles, StylesBase):
|
if not isinstance(styles, StylesBase):
|
||||||
@@ -410,6 +427,44 @@ class StylesBase(ABC):
|
|||||||
|
|
||||||
return size, margin
|
return size, margin
|
||||||
|
|
||||||
|
def align_width(self, width: int, parent_width: int) -> int:
|
||||||
|
"""Align the width dimension.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
width (int): Width of the content.
|
||||||
|
parent_width (int): Width of the parent container.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: An offset to add to the X coordinate.
|
||||||
|
"""
|
||||||
|
offset_x = 0
|
||||||
|
align_horizontal = self.align_horizontal
|
||||||
|
if align_horizontal != "left":
|
||||||
|
if align_horizontal == "center":
|
||||||
|
offset_x = (parent_width - width) // 2
|
||||||
|
else:
|
||||||
|
offset_x = parent_width - width
|
||||||
|
return offset_x
|
||||||
|
|
||||||
|
def align_height(self, height: int, parent_height: int) -> int:
|
||||||
|
"""Align the height dimensions
|
||||||
|
|
||||||
|
Args:
|
||||||
|
height (int): Height of the content.
|
||||||
|
parent_height (int): Height of the parent container.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: An offset to add to the Y coordinate.
|
||||||
|
"""
|
||||||
|
offset_y = 0
|
||||||
|
align_vertical = self.align_vertical
|
||||||
|
if align_vertical != "left":
|
||||||
|
if align_vertical == "middle":
|
||||||
|
offset_y = (parent_height - height) // 2
|
||||||
|
else:
|
||||||
|
offset_y = parent_height - height
|
||||||
|
return offset_y
|
||||||
|
|
||||||
|
|
||||||
@rich.repr.auto
|
@rich.repr.auto
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -676,6 +731,15 @@ class Styles(StylesBase):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if has_rule("align_horizontal") and has_rule("align_vertical"):
|
||||||
|
append_declaration(
|
||||||
|
"align", f"{self.align_horizontal} {self.align_vertical}"
|
||||||
|
)
|
||||||
|
elif has_rule("align_horizontal"):
|
||||||
|
append_declaration("align-horizontal", self.align_horizontal)
|
||||||
|
elif has_rule("align_horizontal"):
|
||||||
|
append_declaration("align-vertical", self.align_vertical)
|
||||||
|
|
||||||
lines.sort()
|
lines.sort()
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ EdgeType = Literal[
|
|||||||
]
|
]
|
||||||
Visibility = Literal["visible", "hidden", "initial", "inherit"]
|
Visibility = Literal["visible", "hidden", "initial", "inherit"]
|
||||||
Display = Literal["block", "none"]
|
Display = Literal["block", "none"]
|
||||||
|
AlignHorizontal = Literal["left", "center", "right"]
|
||||||
|
AlignVertical = Literal["top", "middle", "bottom"]
|
||||||
BoxSizing = Literal["border-box", "content-box"]
|
BoxSizing = Literal["border-box", "content-box"]
|
||||||
Overflow = Literal["scroll", "hidden", "auto"]
|
Overflow = Literal["scroll", "hidden", "auto"]
|
||||||
EdgeStyle = Tuple[str, Color]
|
EdgeStyle = Tuple[str, Color]
|
||||||
|
|||||||
@@ -25,10 +25,14 @@ class HorizontalLayout(Layout):
|
|||||||
parent_size = parent.size
|
parent_size = parent.size
|
||||||
|
|
||||||
for widget in parent.children:
|
for widget in parent.children:
|
||||||
|
styles = widget.styles
|
||||||
(content_width, content_height), margin = widget.styles.get_box_model(
|
(content_width, content_height), margin = widget.styles.get_box_model(
|
||||||
size, parent_size
|
size, parent_size
|
||||||
)
|
)
|
||||||
region = Region(margin.left + x, margin.top, content_width, content_height)
|
offset_y = styles.align_height(content_height, parent_size.height)
|
||||||
|
region = Region(
|
||||||
|
margin.left + x, margin.top + offset_y, content_width, content_height
|
||||||
|
)
|
||||||
max_height = max(max_height, content_height + margin.height)
|
max_height = max(max_height, content_height + margin.height)
|
||||||
add_placement(WidgetPlacement(region, widget, 0))
|
add_placement(WidgetPlacement(region, widget, 0))
|
||||||
x += region.width + margin.left
|
x += region.width + margin.left
|
||||||
|
|||||||
@@ -27,10 +27,15 @@ class VerticalLayout(Layout):
|
|||||||
parent_size = parent.size
|
parent_size = parent.size
|
||||||
|
|
||||||
for widget in parent.children:
|
for widget in parent.children:
|
||||||
(content_width, content_height), margin = widget.styles.get_box_model(
|
styles = widget.styles
|
||||||
|
(content_width, content_height), margin = styles.get_box_model(
|
||||||
size, parent_size
|
size, parent_size
|
||||||
)
|
)
|
||||||
region = Region(margin.left, y + margin.top, content_width, content_height)
|
offset_x = styles.align_width(content_width, parent_size.width)
|
||||||
|
|
||||||
|
region = Region(
|
||||||
|
margin.left + offset_x, y + margin.top, content_width, content_height
|
||||||
|
)
|
||||||
max_width = max(max_width, content_width + margin.width)
|
max_width = max(max_width, content_width + margin.width)
|
||||||
add_placement(WidgetPlacement(region, widget, 0))
|
add_placement(WidgetPlacement(region, widget, 0))
|
||||||
y += region.height + margin.top
|
y += region.height + margin.top
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from rich.console import ConsoleOptions, Console, RenderResult
|
from rich.console import ConsoleOptions, Console, RenderResult
|
||||||
from rich.color import Color
|
|
||||||
from rich.segment import Segment
|
from rich.segment import Segment
|
||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
|
|
||||||
from ._blend_colors import blend_colors_rgb
|
from ..color import Color
|
||||||
|
|
||||||
|
|
||||||
class VerticalGradient:
|
class VerticalGradient:
|
||||||
"""Draw a vertical gradient."""
|
"""Draw a vertical gradient."""
|
||||||
|
|
||||||
def __init__(self, color1: str, color2: str) -> None:
|
def __init__(self, color1: str, color2: str) -> None:
|
||||||
self._color1 = Color.parse(color1).get_truecolor()
|
self._color1 = Color.parse(color1)
|
||||||
self._color2 = Color.parse(color2).get_truecolor()
|
self._color2 = Color.parse(color2)
|
||||||
|
|
||||||
def __rich_console__(
|
def __rich_console__(
|
||||||
self, console: Console, options: ConsoleOptions
|
self, console: Console, options: ConsoleOptions
|
||||||
@@ -22,15 +22,20 @@ class VerticalGradient:
|
|||||||
height = options.height or options.max_height
|
height = options.height or options.max_height
|
||||||
color1 = self._color1
|
color1 = self._color1
|
||||||
color2 = self._color2
|
color2 = self._color2
|
||||||
default_color = Color.default()
|
default_color = Color(0, 0, 0).rich_color
|
||||||
from_color = Style.from_color
|
from_color = Style.from_color
|
||||||
|
blend = color1.blend
|
||||||
|
rich_color1 = color1.rich_color
|
||||||
for y in range(height):
|
for y in range(height):
|
||||||
yield Segment(
|
line_color = from_color(
|
||||||
f"{width * ' '}\n",
|
default_color,
|
||||||
from_color(
|
(
|
||||||
default_color, blend_colors_rgb(color1, color2, y / (height - 1))
|
blend(color2, y / (height - 1)).rich_color
|
||||||
|
if height > 1
|
||||||
|
else rich_color1
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
yield Segment(f"{width * ' '}\n", line_color)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -204,6 +204,10 @@ class Screen(Widget):
|
|||||||
if isinstance(event, events.MouseDown) and widget.can_focus:
|
if isinstance(event, events.MouseDown) and widget.can_focus:
|
||||||
await self.app.set_focus(widget)
|
await self.app.set_focus(widget)
|
||||||
event.style = self.get_style_at(event.screen_x, event.screen_y)
|
event.style = self.get_style_at(event.screen_x, event.screen_y)
|
||||||
|
if widget is self:
|
||||||
|
event.set_forwarded()
|
||||||
|
await self.post_message(event)
|
||||||
|
else:
|
||||||
await widget.forward_event(event.offset(-region.x, -region.y))
|
await widget.forward_event(event.offset(-region.x, -region.y))
|
||||||
|
|
||||||
elif isinstance(event, (events.MouseScrollDown, events.MouseScrollUp)):
|
elif isinstance(event, (events.MouseScrollDown, events.MouseScrollUp)):
|
||||||
|
|||||||
Reference in New Issue
Block a user