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 {
|
#uber1 {
|
||||||
/* border: heavy green; */
|
/* border: heavy green; */
|
||||||
layout: horizontal;
|
layout: vertical;
|
||||||
text: on green;
|
text: on dark_green;
|
||||||
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-item {
|
.list-item {
|
||||||
@@ -9,18 +10,17 @@
|
|||||||
|
|
||||||
/* height: 8; */
|
/* height: 8; */
|
||||||
margin: 1 2;
|
margin: 1 2;
|
||||||
|
text: on dark_blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
#child1 {
|
#child1 {
|
||||||
text: on blue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#child2 {
|
#child2 {
|
||||||
text: on magenta;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#uber2 {
|
/* #uber2 {
|
||||||
margin: 3;
|
margin: 3;
|
||||||
layout: dock;
|
layout: dock;
|
||||||
docks: _default=left;
|
docks: _default=left;
|
||||||
}
|
} */
|
||||||
|
|||||||
@@ -20,7 +20,12 @@ class BasicApp(App):
|
|||||||
self.mount(
|
self.mount(
|
||||||
uber1=Widget(
|
uber1=Widget(
|
||||||
Placeholder(id="child1", classes={"list-item"}),
|
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"}),
|
# Placeholder(id="child3", classes={"list-item"}),
|
||||||
),
|
),
|
||||||
# uber2=uber2,
|
# uber2=uber2,
|
||||||
|
|||||||
@@ -207,10 +207,7 @@ class Compositor:
|
|||||||
placements, arranged_widgets = widget.layout.arrange(
|
placements, arranged_widgets = widget.layout.arrange(
|
||||||
widget, region.size, scroll
|
widget, region.size, scroll
|
||||||
)
|
)
|
||||||
for placement in placements:
|
|
||||||
log(placement=placement)
|
|
||||||
|
|
||||||
log(arranged=arranged_widgets)
|
|
||||||
widgets.update(arranged_widgets)
|
widgets.update(arranged_widgets)
|
||||||
placements = sorted(placements, key=attrgetter("order"))
|
placements = sorted(placements, key=attrgetter("order"))
|
||||||
|
|
||||||
@@ -226,8 +223,6 @@ class Compositor:
|
|||||||
return total_region.size
|
return total_region.size
|
||||||
|
|
||||||
virtual_size = add_widget(root, size.region, (), size.region)
|
virtual_size = add_widget(root, size.region, (), size.region)
|
||||||
# for widget, placement in map.items():
|
|
||||||
# log("*", widget, placement)
|
|
||||||
return map, virtual_size, widgets
|
return map, virtual_size, widgets
|
||||||
|
|
||||||
async def mount_all(self, screen: Screen) -> None:
|
async def mount_all(self, screen: Screen) -> None:
|
||||||
@@ -319,9 +314,7 @@ class Compositor:
|
|||||||
|
|
||||||
for region, order, clip in self.map.values():
|
for region, order, clip in self.map.values():
|
||||||
region = region.intersection(clip)
|
region = region.intersection(clip)
|
||||||
log(clipped=region, bool=bool(region and (region in screen_region)))
|
|
||||||
if region and (region in screen_region):
|
if region and (region in screen_region):
|
||||||
log(1)
|
|
||||||
region_cuts = (region.x, region.x + region.width)
|
region_cuts = (region.x, region.x + region.width)
|
||||||
for cut in cuts[region.y : region.y + region.height]:
|
for cut in cuts[region.y : region.y + region.height]:
|
||||||
cut.extend(region_cuts)
|
cut.extend(region_cuts)
|
||||||
@@ -372,13 +365,17 @@ class Compositor:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def _assemble_chops(
|
def _assemble_chops(
|
||||||
cls, chops: list[dict[int, list[Segment] | None]]
|
cls, chops: list[dict[int, list[Segment] | None]]
|
||||||
) -> Iterable[Iterable[Segment]]:
|
) -> list[list[Segment]]:
|
||||||
|
|
||||||
from_iterable = chain.from_iterable
|
# Pretty sure we don't need to sort the buck items
|
||||||
for bucket in chops:
|
segment_lines = [
|
||||||
yield from_iterable(
|
sum(
|
||||||
line for _, line in sorted(bucket.items()) if line is not None
|
(line for _, line in bucket.items() if line is not None),
|
||||||
|
start=[],
|
||||||
)
|
)
|
||||||
|
for bucket in chops
|
||||||
|
]
|
||||||
|
return segment_lines
|
||||||
|
|
||||||
def render(
|
def render(
|
||||||
self,
|
self,
|
||||||
@@ -406,9 +403,11 @@ class Compositor:
|
|||||||
|
|
||||||
# Maps each cut on to a list of segments
|
# Maps each cut on to a list of segments
|
||||||
cuts = self.cuts
|
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(
|
fromkeys = cast(
|
||||||
Callable[[list[int]], dict[int, list[Segment] | None]], dict.fromkeys
|
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]] = [
|
chops: list[dict[int, list[Segment] | None]] = [
|
||||||
fromkeys(cut_set) for cut_set in cuts
|
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)]
|
final_cuts = [cut for cut in cuts[y] if (last_cut >= cut >= first_cut)]
|
||||||
|
|
||||||
if len(final_cuts) == 2:
|
if len(final_cuts) == 2:
|
||||||
|
# Two cuts, which means the entire line
|
||||||
cut_segments = [line]
|
cut_segments = [line]
|
||||||
else:
|
else:
|
||||||
|
# More than one cut, which means we need to divide the line
|
||||||
render_x = render_region.x
|
render_x = render_region.x
|
||||||
relative_cuts = [cut - render_x for cut in final_cuts]
|
relative_cuts = [cut - render_x for cut in final_cuts]
|
||||||
_, *cut_segments = divide(line, relative_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):
|
for cut, segments in zip(final_cuts, cut_segments):
|
||||||
if chops[y][cut] is None:
|
if chops[y][cut] is None:
|
||||||
chops[y][cut] = segments
|
chops[y][cut] = segments
|
||||||
|
|
||||||
# Assemble the cut renders in to lists of segments
|
# Assemble the cut renders in to lists of segments
|
||||||
crop_x, crop_y, crop_x2, crop_y2 = crop_region.corners
|
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]:
|
def width_view(line: list[Segment]) -> list[Segment]:
|
||||||
if line:
|
if line:
|
||||||
@@ -451,9 +453,7 @@ class Compositor:
|
|||||||
return line
|
return line
|
||||||
|
|
||||||
if crop is not None and (crop_x, crop_x2) != (0, self.width):
|
if crop is not None and (crop_x, crop_x2) != (0, self.width):
|
||||||
render_lines = [width_view(line) for line in output_lines]
|
render_lines = [width_view(line) for line in render_lines]
|
||||||
else:
|
|
||||||
render_lines = list(output_lines)
|
|
||||||
|
|
||||||
return SegmentLines(render_lines, new_lines=True)
|
return SegmentLines(render_lines, new_lines=True)
|
||||||
|
|
||||||
@@ -463,14 +463,21 @@ class Compositor:
|
|||||||
yield self.render(console)
|
yield self.render(console)
|
||||||
|
|
||||||
def update_widget(self, console: Console, widget: Widget) -> LayoutUpdate | None:
|
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:
|
if widget not in self.regions:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
region, clip = self.regions[widget]
|
region, clip = self.regions[widget]
|
||||||
|
if not region:
|
||||||
if not region.size:
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
update_region = region.intersection(clip)
|
update_region = region.intersection(clip)
|
||||||
if not update_region:
|
if not update_region:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from .constants import (
|
|||||||
VALID_BOX_SIZING,
|
VALID_BOX_SIZING,
|
||||||
VALID_EDGE,
|
VALID_EDGE,
|
||||||
VALID_DISPLAY,
|
VALID_DISPLAY,
|
||||||
|
VALID_OVERFLOW,
|
||||||
VALID_VISIBILITY,
|
VALID_VISIBILITY,
|
||||||
)
|
)
|
||||||
from .errors import DeclarationError
|
from .errors import DeclarationError
|
||||||
@@ -20,7 +21,7 @@ from .scalar import Scalar, ScalarOffset, Unit, ScalarError
|
|||||||
from .styles import DockGroup, Styles
|
from .styles import DockGroup, Styles
|
||||||
from .tokenize import Token
|
from .tokenize import Token
|
||||||
from .transition import Transition
|
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 .._duration import _duration_as_seconds
|
||||||
from .._easing import EASING
|
from .._easing import EASING
|
||||||
from ..geometry import Spacing, SpacingDimensions, clamp
|
from ..geometry import Spacing, SpacingDimensions, clamp
|
||||||
@@ -83,6 +84,40 @@ class StylesBuilder:
|
|||||||
except Exception as error:
|
except Exception as error:
|
||||||
self.error(declaration.name, declaration.token, str(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:
|
def process_display(self, name: str, tokens: list[Token], important: bool) -> None:
|
||||||
for token in tokens:
|
for token in tokens:
|
||||||
name, value, _, _, location, _ = token
|
name, value, _, _, location, _ = token
|
||||||
@@ -153,6 +188,20 @@ class StylesBuilder:
|
|||||||
) -> None:
|
) -> None:
|
||||||
self._process_scalar(name, tokens)
|
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(
|
def process_visibility(
|
||||||
self, name: str, tokens: list[Token], important: bool
|
self, name: str, tokens: list[Token], important: bool
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ VALID_EDGE: Final = {"top", "right", "bottom", "left"}
|
|||||||
VALID_LAYOUT: Final = {"dock", "vertical", "grid"}
|
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"}
|
||||||
|
|
||||||
|
|
||||||
NULL_SPACING: Final = Spacing(0, 0, 0, 0)
|
NULL_SPACING: Final = Spacing(0, 0, 0, 0)
|
||||||
|
|||||||
@@ -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 ..geometry import Region, Size, Spacing
|
from ..geometry import Size, Spacing
|
||||||
from ._style_properties import (
|
from ._style_properties import (
|
||||||
BorderProperty,
|
BorderProperty,
|
||||||
BoxProperty,
|
BoxProperty,
|
||||||
@@ -32,11 +32,19 @@ from ._style_properties import (
|
|||||||
TransitionsProperty,
|
TransitionsProperty,
|
||||||
FractionalProperty,
|
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 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 BoxSizing, Display, Edge, Specificity3, Specificity4, Visibility
|
from .types import (
|
||||||
|
BoxSizing,
|
||||||
|
Display,
|
||||||
|
Edge,
|
||||||
|
Overflow,
|
||||||
|
Specificity3,
|
||||||
|
Specificity4,
|
||||||
|
Visibility,
|
||||||
|
)
|
||||||
|
|
||||||
if sys.version_info >= (3, 8):
|
if sys.version_info >= (3, 8):
|
||||||
from typing import TypedDict
|
from typing import TypedDict
|
||||||
@@ -92,6 +100,9 @@ class RulesMap(TypedDict, total=False):
|
|||||||
dock: str
|
dock: str
|
||||||
docks: tuple[DockGroup, ...]
|
docks: tuple[DockGroup, ...]
|
||||||
|
|
||||||
|
overflow_x: Overflow
|
||||||
|
overflow_y: Overflow
|
||||||
|
|
||||||
layers: tuple[str, ...]
|
layers: tuple[str, ...]
|
||||||
layer: str
|
layer: str
|
||||||
|
|
||||||
@@ -161,6 +172,9 @@ class StylesBase(ABC):
|
|||||||
dock = DockProperty()
|
dock = DockProperty()
|
||||||
docks = DocksProperty()
|
docks = DocksProperty()
|
||||||
|
|
||||||
|
overflow_x = StringEnumProperty(VALID_OVERFLOW, "hidden")
|
||||||
|
overflow_y = StringEnumProperty(VALID_OVERFLOW, "hidden")
|
||||||
|
|
||||||
layer = NameProperty()
|
layer = NameProperty()
|
||||||
layers = NameListProperty()
|
layers = NameListProperty()
|
||||||
transitions = TransitionsProperty()
|
transitions = TransitionsProperty()
|
||||||
@@ -638,6 +652,11 @@ class Styles(StylesBase):
|
|||||||
if has_rule("text_style"):
|
if has_rule("text_style"):
|
||||||
append_declaration("text-style", str(get_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"):
|
if has_rule("box-sizing"):
|
||||||
append_declaration("box-sizing", self.box_sizing)
|
append_declaration("box-sizing", self.box_sizing)
|
||||||
if has_rule("width"):
|
if has_rule("width"):
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ Edge = Literal["top", "right", "bottom", "left"]
|
|||||||
Visibility = Literal["visible", "hidden", "initial", "inherit"]
|
Visibility = Literal["visible", "hidden", "initial", "inherit"]
|
||||||
Display = Literal["block", "none"]
|
Display = Literal["block", "none"]
|
||||||
BoxSizing = Literal["border-box", "content-box"]
|
BoxSizing = Literal["border-box", "content-box"]
|
||||||
|
Overflow = Literal["scroll", "hidden", "auto"]
|
||||||
EdgeStyle = Tuple[str, Color]
|
EdgeStyle = Tuple[str, Color]
|
||||||
Specificity3 = Tuple[int, int, int]
|
Specificity3 = Tuple[int, int, int]
|
||||||
Specificity4 = Tuple[int, 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)
|
region = Region(margin.left, 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.y_max
|
y += region.height + margin.top
|
||||||
max_height = y + margin.bottom
|
max_height = y + margin.bottom
|
||||||
|
|
||||||
total_region = Region(0, 0, max_width, max_height)
|
total_region = Region(0, 0, max_width, max_height)
|
||||||
|
|||||||
@@ -121,8 +121,6 @@ class Widget(DOMNode):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
renderable = self.render()
|
renderable = self.render()
|
||||||
self.log(renderable)
|
|
||||||
|
|
||||||
styles = self.styles
|
styles = self.styles
|
||||||
|
|
||||||
parent_text_style = self.parent.text_style
|
parent_text_style = self.parent.text_style
|
||||||
|
|||||||
Reference in New Issue
Block a user