mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
added overflow to CSS
This commit is contained in:
@@ -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;
|
||||
}
|
||||
} */
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -121,8 +121,6 @@ class Widget(DOMNode):
|
||||
"""
|
||||
|
||||
renderable = self.render()
|
||||
self.log(renderable)
|
||||
|
||||
styles = self.styles
|
||||
|
||||
parent_text_style = self.parent.text_style
|
||||
|
||||
Reference in New Issue
Block a user