diff --git a/sandbox/uber.css b/sandbox/uber.css index f27e16262..7739d920b 100644 --- a/sandbox/uber.css +++ b/sandbox/uber.css @@ -1,7 +1,8 @@ #uber1 { /* border: heavy green; */ - layout: horizontal; - text: on green; + layout: vertical; + text: on dark_green; + overflow-y: scroll; } .list-item { @@ -9,18 +10,17 @@ /* height: 8; */ margin: 1 2; + text: on dark_blue; } #child1 { - text: on blue; } #child2 { - text: on magenta; } -#uber2 { +/* #uber2 { margin: 3; layout: dock; docks: _default=left; -} +} */ diff --git a/sandbox/uber.py b/sandbox/uber.py index 1115f7d33..ecb28455c 100644 --- a/sandbox/uber.py +++ b/sandbox/uber.py @@ -20,7 +20,12 @@ class BasicApp(App): self.mount( uber1=Widget( Placeholder(id="child1", classes={"list-item"}), - Widget(id="child2", classes={"list-item"}), + Placeholder(id="child2", classes={"list-item"}), + Placeholder(id="child3", classes={"list-item"}), + Placeholder(classes={"list-item"}), + Placeholder(classes={"list-item"}), + Placeholder(classes={"list-item"}), + Placeholder(classes={"list-item"}), # Placeholder(id="child3", classes={"list-item"}), ), # uber2=uber2, diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index f309f76dc..2240256f1 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -207,10 +207,7 @@ class Compositor: placements, arranged_widgets = widget.layout.arrange( widget, region.size, scroll ) - for placement in placements: - log(placement=placement) - log(arranged=arranged_widgets) widgets.update(arranged_widgets) placements = sorted(placements, key=attrgetter("order")) @@ -226,8 +223,6 @@ class Compositor: return total_region.size virtual_size = add_widget(root, size.region, (), size.region) - # for widget, placement in map.items(): - # log("*", widget, placement) return map, virtual_size, widgets async def mount_all(self, screen: Screen) -> None: @@ -319,9 +314,7 @@ class Compositor: for region, order, clip in self.map.values(): region = region.intersection(clip) - log(clipped=region, bool=bool(region and (region in screen_region))) if region and (region in screen_region): - log(1) region_cuts = (region.x, region.x + region.width) for cut in cuts[region.y : region.y + region.height]: cut.extend(region_cuts) @@ -372,13 +365,17 @@ class Compositor: @classmethod def _assemble_chops( cls, chops: list[dict[int, list[Segment] | None]] - ) -> Iterable[Iterable[Segment]]: + ) -> list[list[Segment]]: - from_iterable = chain.from_iterable - for bucket in chops: - yield from_iterable( - line for _, line in sorted(bucket.items()) if line is not None + # Pretty sure we don't need to sort the buck items + segment_lines = [ + sum( + (line for _, line in bucket.items() if line is not None), + start=[], ) + for bucket in chops + ] + return segment_lines def render( self, @@ -406,9 +403,11 @@ class Compositor: # Maps each cut on to a list of segments cuts = self.cuts + # dict.fromkeys is a callable which takes a list of ints returns a dict which maps ints on to a list of Segments or None. fromkeys = cast( Callable[[list[int]], dict[int, list[Segment] | None]], dict.fromkeys ) + # A mapping of cut index to a list of segments for each line chops: list[dict[int, list[Segment] | None]] = [ fromkeys(cut_set) for cut_set in cuts ] @@ -431,18 +430,21 @@ class Compositor: final_cuts = [cut for cut in cuts[y] if (last_cut >= cut >= first_cut)] if len(final_cuts) == 2: + # Two cuts, which means the entire line cut_segments = [line] else: + # More than one cut, which means we need to divide the line render_x = render_region.x relative_cuts = [cut - render_x for cut in final_cuts] _, *cut_segments = divide(line, relative_cuts) + # Since we are painting front to back, the first segments for a cut "wins" for cut, segments in zip(final_cuts, cut_segments): if chops[y][cut] is None: chops[y][cut] = segments # Assemble the cut renders in to lists of segments crop_x, crop_y, crop_x2, crop_y2 = crop_region.corners - output_lines = self._assemble_chops(chops[crop_y:crop_y2]) + render_lines = self._assemble_chops(chops[crop_y:crop_y2]) def width_view(line: list[Segment]) -> list[Segment]: if line: @@ -451,9 +453,7 @@ class Compositor: return line if crop is not None and (crop_x, crop_x2) != (0, self.width): - render_lines = [width_view(line) for line in output_lines] - else: - render_lines = list(output_lines) + render_lines = [width_view(line) for line in render_lines] return SegmentLines(render_lines, new_lines=True) @@ -463,14 +463,21 @@ class Compositor: yield self.render(console) def update_widget(self, console: Console, widget: Widget) -> LayoutUpdate | None: + """Update a given widget in the composition. + + Args: + console (Console): Console instance. + widget (Widget): Widget to update. + + Returns: + LayoutUpdate | None: A renderable or None if nothing to render. + """ if widget not in self.regions: return None region, clip = self.regions[widget] - - if not region.size: + if not region: return None - update_region = region.intersection(clip) if not update_region: return None diff --git a/src/textual/css/_styles_builder.py b/src/textual/css/_styles_builder.py index f4137abe3..b15215856 100644 --- a/src/textual/css/_styles_builder.py +++ b/src/textual/css/_styles_builder.py @@ -12,6 +12,7 @@ from .constants import ( VALID_BOX_SIZING, VALID_EDGE, VALID_DISPLAY, + VALID_OVERFLOW, VALID_VISIBILITY, ) from .errors import DeclarationError @@ -20,7 +21,7 @@ from .scalar import Scalar, ScalarOffset, Unit, ScalarError from .styles import DockGroup, Styles from .tokenize import Token from .transition import Transition -from .types import BoxSizing, Edge, Display, Visibility +from .types import BoxSizing, Edge, Display, Overflow, Visibility from .._duration import _duration_as_seconds from .._easing import EASING from ..geometry import Spacing, SpacingDimensions, clamp @@ -83,6 +84,40 @@ class StylesBuilder: except Exception as error: self.error(declaration.name, declaration.token, str(error)) + def _process_enum( + self, name: str, tokens: list[Token], valid_values: set[str] + ) -> str: + """Process a declaration that expects an enum. + + Args: + name (str): Name of declaration. + tokens (list[Token]): Tokens from parser. + valid_values (list[str]): A set of valid values. + + Returns: + bool: True if the value is valid or False if it is invalid (also generates an error) + """ + + if len(tokens) != 1: + self.error(name, tokens[0], "expected a single token here") + return False + + token = tokens[0] + token_name, value, _, _, location, _ = token + if token_name != "token": + self.error( + name, + token, + f"invalid token {value!r}, expected {friendly_list(valid_values)}", + ) + if value not in valid_values: + self.error( + name, + token, + f"invalid value {value!r} for {name}, expected {friendly_list(valid_values)}", + ) + return value + def process_display(self, name: str, tokens: list[Token], important: bool) -> None: for token in tokens: name, value, _, _, location, _ = token @@ -153,6 +188,20 @@ class StylesBuilder: ) -> None: self._process_scalar(name, tokens) + def process_overflow_x( + self, name: str, tokens: list[Token], important: bool + ) -> None: + self.styles._rules["overflow_x"] = cast( + Overflow, self._process_enum(name, tokens, VALID_OVERFLOW) + ) + + def process_overflow_y( + self, name: str, tokens: list[Token], important: bool + ) -> None: + self.styles._rules["overflow_y"] = cast( + Overflow, self._process_enum(name, tokens, VALID_OVERFLOW) + ) + def process_visibility( self, name: str, tokens: list[Token], important: bool ) -> None: diff --git a/src/textual/css/constants.py b/src/textual/css/constants.py index 2fa415997..adeba8731 100644 --- a/src/textual/css/constants.py +++ b/src/textual/css/constants.py @@ -25,6 +25,7 @@ VALID_EDGE: Final = {"top", "right", "bottom", "left"} VALID_LAYOUT: Final = {"dock", "vertical", "grid"} VALID_BOX_SIZING: Final = {"border-box", "content-box"} +VALID_OVERFLOW: Final = {"scroll", "hidden", "auto"} NULL_SPACING: Final = Spacing(0, 0, 0, 0) diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py index 37c51b426..d180bb932 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -13,7 +13,7 @@ from rich.style import Style from .. import log from .._animator import Animation, EasingFunction -from ..geometry import Region, Size, Spacing +from ..geometry import Size, Spacing from ._style_properties import ( BorderProperty, BoxProperty, @@ -32,11 +32,19 @@ from ._style_properties import ( TransitionsProperty, FractionalProperty, ) -from .constants import VALID_BOX_SIZING, VALID_DISPLAY, VALID_VISIBILITY +from .constants import VALID_BOX_SIZING, VALID_DISPLAY, VALID_VISIBILITY, VALID_OVERFLOW from .scalar import Scalar, ScalarOffset, Unit from .scalar_animation import ScalarAnimation from .transition import Transition -from .types import BoxSizing, Display, Edge, Specificity3, Specificity4, Visibility +from .types import ( + BoxSizing, + Display, + Edge, + Overflow, + Specificity3, + Specificity4, + Visibility, +) if sys.version_info >= (3, 8): from typing import TypedDict @@ -92,6 +100,9 @@ class RulesMap(TypedDict, total=False): dock: str docks: tuple[DockGroup, ...] + overflow_x: Overflow + overflow_y: Overflow + layers: tuple[str, ...] layer: str @@ -161,6 +172,9 @@ class StylesBase(ABC): dock = DockProperty() docks = DocksProperty() + overflow_x = StringEnumProperty(VALID_OVERFLOW, "hidden") + overflow_y = StringEnumProperty(VALID_OVERFLOW, "hidden") + layer = NameProperty() layers = NameListProperty() transitions = TransitionsProperty() @@ -638,6 +652,11 @@ class Styles(StylesBase): if has_rule("text_style"): append_declaration("text-style", str(get_rule("text_style"))) + if has_rule("overflow-x"): + append_declaration("overflow-x", self.overflow_x) + if has_rule("overflow-y"): + append_declaration("overflow-y", self.overflow_y) + if has_rule("box-sizing"): append_declaration("box-sizing", self.box_sizing) if has_rule("width"): diff --git a/src/textual/css/types.py b/src/textual/css/types.py index da170922c..9c257657b 100644 --- a/src/textual/css/types.py +++ b/src/textual/css/types.py @@ -16,6 +16,7 @@ Edge = Literal["top", "right", "bottom", "left"] Visibility = Literal["visible", "hidden", "initial", "inherit"] Display = Literal["block", "none"] BoxSizing = Literal["border-box", "content-box"] +Overflow = Literal["scroll", "hidden", "auto"] EdgeStyle = Tuple[str, Color] Specificity3 = Tuple[int, int, int] Specificity4 = Tuple[int, int, int, int] diff --git a/src/textual/layouts/vertical.py b/src/textual/layouts/vertical.py index 4d964d99d..197359c53 100644 --- a/src/textual/layouts/vertical.py +++ b/src/textual/layouts/vertical.py @@ -35,7 +35,7 @@ class VerticalLayout(Layout): region = Region(margin.left, y + margin.top, content_width, content_height) max_width = max(max_width, content_width + margin.width) add_placement(WidgetPlacement(region, widget, 0)) - y += region.y_max + y += region.height + margin.top max_height = y + margin.bottom total_region = Region(0, 0, max_width, max_height) diff --git a/src/textual/widget.py b/src/textual/widget.py index 6e001a3f6..1b023c956 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -121,8 +121,6 @@ class Widget(DOMNode): """ renderable = self.render() - self.log(renderable) - styles = self.styles parent_text_style = self.parent.text_style