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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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