added overflow to CSS

This commit is contained in:
Will McGugan
2022-03-08 14:48:45 +00:00
parent 2b08484932
commit 5f3720ed0e
9 changed files with 113 additions and 33 deletions

View File

@@ -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;
}
} */

View File

@@ -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,

View File

@@ -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

View File

@@ -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:

View File

@@ -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)

View File

@@ -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"):

View File

@@ -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]

View File

@@ -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)

View File

@@ -121,8 +121,6 @@ class Widget(DOMNode):
"""
renderable = self.render()
self.log(renderable)
styles = self.styles
parent_text_style = self.parent.text_style