implemented outline

This commit is contained in:
Will McGugan
2022-07-02 14:37:00 +01:00
parent 81481a0e16
commit 97c58a7b0a
6 changed files with 156 additions and 81 deletions

View File

@@ -16,7 +16,8 @@
}
*:hover {
/* tint: 30% red; */
tint: 30% red;
/* outline: heavy red; */
}
App > Screen {
@@ -224,13 +225,15 @@ Warning {
Success {
width: 100%;
height:3;
width:90%;
height:auto;
box-sizing: border-box;
background: $success-lighten-3;
color: $text-success-lighten-3-fade-1;
border-top: hkey $success;
border-bottom: hkey $success;
margin: 1 2;
text-style: bold;
align-horizontal: center;
}

View File

@@ -88,7 +88,7 @@ class Warning(Widget):
class Success(Widget):
def render(self) -> Text:
return Text("This is a success message", justify="center")
return Text("This\nis\na\nsuccess\n message", justify="center")
class BasicApp(App, css_path="basic.css"):

View File

@@ -70,15 +70,19 @@ INVISIBLE_EDGE_TYPES = cast("frozenset[EdgeType]", frozenset(("", "none", "hidde
BorderValue: TypeAlias = Tuple[EdgeType, Union[str, Color, Style]]
BoxSegments: TypeAlias = tuple[
tuple[Segment, Segment, Segment],
tuple[Segment, Segment, Segment],
tuple[Segment, Segment, Segment],
]
Borders: TypeAlias = tuple[EdgeStyle, EdgeStyle, EdgeStyle, EdgeStyle]
@lru_cache(maxsize=1024)
def get_box(
name: EdgeType, inner_style: Style, outer_style: Style, style: Style
) -> tuple[
tuple[Segment, Segment, Segment],
tuple[Segment, Segment, Segment],
tuple[Segment, Segment, Segment],
]:
) -> BoxSegments:
"""Get segments used to render a box.
Args:
@@ -124,6 +128,20 @@ def get_box(
)
def render_row(
box_row: tuple[Segment, Segment, Segment], width: int, left: bool, right: bool
) -> list[Segment]:
box1, box2, box3 = box_row
if left and right:
return [box1, Segment(box2.text * (width - 2), box2.style), box3]
if left:
return [box1, Segment(box2.text * (width - 1), box2.style)]
if right:
return [Segment(box2.text * (width - 1), box2.style), box3]
else:
return [Segment(box2.text * width, box2.style)]
@rich.repr.auto
class Border:
"""Renders Textual CSS borders.
@@ -137,13 +155,13 @@ class Border:
def __init__(
self,
renderable: RenderableType,
edge_styles: tuple[EdgeStyle, EdgeStyle, EdgeStyle, EdgeStyle],
borders: Borders,
inner_color: Color,
outer_color: Color,
outline: bool = False,
):
self.renderable = renderable
self.edge_styles = edge_styles
self.edge_styles = borders
self.outline = outline
(
@@ -151,7 +169,7 @@ class Border:
(right, right_color),
(bottom, bottom_color),
(left, left_color),
) = edge_styles
) = borders
self._sides: tuple[EdgeType, EdgeType, EdgeType, EdgeType]
self._sides = (top, right, bottom, left)
from_color = Style.from_color

View File

@@ -4,7 +4,10 @@ Tools for processing Segments, or lists of Segments.
from __future__ import annotations
from typing import Iterable
from rich.segment import Segment
from rich.style import Style
from ._cells import cell_len
@@ -88,3 +91,36 @@ def line_trim(segments: list[Segment], start: bool, end: bool) -> list[Segment]:
else:
segments.pop()
return segments
def line_pad(
segments: Iterable[Segment], pad_left: int, pad_right: int, style: Style
) -> Iterable[Segment]:
"""Adds padding to the left and / or right of a list of segments.
Args:
segments (list[Segment]): A line (list of Segments).
pad_left (int): Cells to pad on the left.
pad_right (int): Cells to pad on the right.
style (Style): Style of padded cells.
Returns:
list[Segment]: A new line with padding.
"""
if pad_left and pad_right:
segments = [
Segment(" " * pad_left, style),
*segments,
Segment(" " * pad_right, style),
]
elif pad_left:
segments = [
Segment(" " * pad_left, style),
*segments,
]
elif pad_right:
segments = [
*segments,
Segment(" " * pad_right, style),
]
return segments

View File

@@ -5,12 +5,12 @@ from typing import Iterable, TYPE_CHECKING
from rich.segment import Segment
from ._border import get_box
from ._border import get_box, render_row
from .color import Color
from .css.types import EdgeType
from .renderables.opacity import Opacity
from .renderables.tint import Tint
from ._segment_tools import line_crop
from ._segment_tools import line_crop, line_pad, line_trim
from ._types import Lines
from .geometry import Region, Size
@@ -20,9 +20,6 @@ if TYPE_CHECKING:
from .widget import Widget
NORMALIZE_BORDER: dict[EdgeType, EdgeType] = {"none": "", "hidden": ""}
class StylesRenderer:
"""Responsible for rendering CSS Styles and keeping a cached of rendered lines."""
@@ -95,7 +92,6 @@ class StylesRenderer:
gutter = styles.gutter
width, height = size
content_width, content_height = size - gutter.totals
pad_top, pad_right, pad_bottom, pad_left = styles.padding
(
@@ -105,11 +101,12 @@ class StylesRenderer:
(border_left, border_left_color),
) = styles.border
normalize_border_get = NORMALIZE_BORDER.get
border_top = normalize_border_get(border_top, border_top)
border_right = normalize_border_get(border_right, border_right)
border_bottom = normalize_border_get(border_bottom, border_bottom)
border_left = normalize_border_get(border_left, border_left)
(
(outline_top, outline_top_color),
(outline_right, outline_right_color),
(outline_bottom, outline_bottom_color),
(outline_left, outline_left_color),
) = styles.outline
from_color = Style.from_color
@@ -124,8 +121,9 @@ class StylesRenderer:
segments = Tint.process_segments(segments, styles.tint)
return segments if isinstance(segments, list) else list(segments)
line: Iterable[Segment]
# Draw top or bottom borders
if border_top and y in (0, height - 1):
if (border_top and y == 0) or (border_bottom and y == height - 1):
border_color = border_top_color if y == 0 else border_bottom_color
box_segments = get_box(
@@ -134,19 +132,17 @@ class StylesRenderer:
outer_style,
from_color(color=border_color.rich_color),
)
box1, box2, box3 = box_segments[0 if y == 0 else 2]
if border_left and border_right:
return post([box1, Segment(box2.text * (width - 2), box2.style), box3])
elif border_left:
return post([box1, Segment(box2.text * (width - 1), box2.style)])
elif border_right:
return post([Segment(box2.text * (width - 1), box2.style), box3])
else:
return post([Segment(box2.text * width, box2.style)])
line = render_row(
box_segments[0 if y == 0 else 2],
width,
border_left != "",
border_right != "",
)
# Draw padding
if (pad_top and y < gutter.top) or (pad_bottom and y >= height - gutter.bottom):
elif (pad_top and y < gutter.top) or (
pad_bottom and y >= height - gutter.bottom
):
background_style = from_color(
color=rich_style.color, bgcolor=background.rich_color
)
@@ -163,59 +159,79 @@ class StylesRenderer:
from_color(color=border_right_color.rich_color),
)
if border_left and border_right:
return post([left, Segment(" " * (width - 2), background_style), right])
line = [left, Segment(" " * (width - 2), background_style), right]
if border_left:
return post([left, Segment(" " * (width - 1), background_style)])
line = [left, Segment(" " * (width - 1), background_style)]
if border_right:
return post([Segment(" " * (width - 1), background_style), right])
return post([Segment(" " * width, background_style)])
line = [Segment(" " * (width - 1), background_style), right]
else:
line = [Segment(" " * width, background_style)]
else:
# Apply background style
line = self.render_content_line(y - gutter.top)
if inner_style:
line = Segment.apply_style(line, inner_style)
line = line_pad(line, pad_left, pad_right, inner_style)
# Apply background style
line = self.render_content_line(y - gutter.top)
if inner_style:
line = list(Segment.apply_style(line, inner_style))
if border_left or border_right:
# Add left / right border
_, (left, _, _), _ = get_box(
border_left,
inner_style,
outer_style,
from_color(border_left_color.rich_color),
)
_, (_, _, right), _ = get_box(
border_right,
inner_style,
outer_style,
from_color(border_right_color.rich_color),
)
# Add padding
if pad_left and pad_right:
line = [
Segment(" " * pad_left, inner_style),
*line,
Segment(" " * pad_right, inner_style),
]
elif pad_left:
line = [
Segment(" " * pad_left, inner_style),
*line,
]
elif pad_right:
line = [
*line,
Segment(" " * pad_right, inner_style),
]
if border_left and border_right:
line = [left, *line, right]
elif border_left:
line = [left, *line]
else:
line = [*line, right]
if not border_left and not border_right:
return post(line)
if (outline_top and y == 0) or (outline_bottom and y == height - 1):
outline_color = outline_top_color if y == 0 else outline_bottom_color
box_segments = get_box(
outline_top if y == 0 else outline_bottom,
inner_style,
outer_style,
from_color(color=outline_color.rich_color),
)
line = render_row(
box_segments[0 if y == 0 else 2],
width,
outline_left != "",
outline_right != "",
)
# Add left / right border
_, (left, _, _), _ = get_box(
border_left,
inner_style,
outer_style,
from_color(border_left_color.rich_color),
)
_, (_, _, right), _ = get_box(
border_right,
inner_style,
outer_style,
from_color(border_right_color.rich_color),
)
elif outline_left or outline_right:
_, (left, _, _), _ = get_box(
outline_left,
inner_style,
outer_style,
from_color(outline_left_color.rich_color),
)
_, (_, _, right), _ = get_box(
outline_right,
inner_style,
outer_style,
from_color(outline_right_color.rich_color),
)
line = line_trim(list(line), outline_left != "", outline_right != "")
if outline_left and outline_right:
line = [left, *line, right]
elif outline_left:
line = [left, *line]
else:
line = [*line, right]
if border_left and border_right:
return post([left, *line, right])
elif border_left:
return post([left, *line])
return post([*line, right])
return post(line)
if __name__ == "__main__":

View File

@@ -221,6 +221,8 @@ class BoxProperty:
it's style. Example types are "rounded", "solid", and "dashed".
"""
box_type, color = obj.get_rule(self.name) or ("", self._default_color)
if box_type in {"none", "hidden"}:
box_type = ""
return (box_type, color)
def __set__(self, obj: Styles, border: tuple[EdgeType, str | Color] | None):