Skip to content
Merged
118 changes: 37 additions & 81 deletions src/textual/_compositor.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
from textual._spatial_map import SpatialMap
from textual.geometry import NULL_OFFSET, NULL_SPACING, Offset, Region, Size, Spacing
from textual.map_geometry import MapGeometry
from textual.layout import WidgetPlacement
from textual.strip import Strip, StripRenderable
from textual.widget import Widget

Expand Down Expand Up @@ -1179,7 +1180,6 @@ def arrange_widget(

# Original arrangement ordinals survive spatial admission.
# Do not rebuild ranks for every offscreen child on scroll.
first_layer_order = layer_order - len(arrange_result.placements) + 1

arranged_widgets = arrange_result.widgets
widgets.update(arranged_widgets)
Expand Down Expand Up @@ -1208,16 +1208,13 @@ def arrange_widget(
placements = arrange_result.placements
total_region = total_region.union(arrange_result.total_region)

# An offset added to all placements
placement_offset = container_region.offset
placement_scroll_offset = placement_offset - widget.scroll_offset
placement_scroll_offset = container_region.offset - widget.scroll_offset

screen_coordinates.update(placement.widget for _, placement in placements
if placement.widget.uses_screen_coordinates)
placements = [
(ordinal, placement.process_offset(size.region, placement_scroll_offset))
for ordinal, placement in placements
]
placements = WidgetPlacement.process_offsets(
placements, size.region, placement_scroll_offset,
)

if type(widget).layers is Widget.layers:
layers_to_index = default_layers if inherited_layers is None else inherited_layers
Expand All @@ -1228,8 +1225,6 @@ def arrange_widget(
for index, layer_name in enumerate(widget.layers)
}

get_layer_index = layers_to_index.get

if widget._cover_widget is not None:
store_geometry(widget._cover_widget, _MapGeometry(
region.shrink(widget.styles.gutter),
Expand All @@ -1242,38 +1237,20 @@ def arrange_widget(
), clip)

# Add all the widgets
for ordinal, (
sub_region,
sub_region_offset,
_,
sub_widget,
z,
fixed,
overlay,
absolute,
) in reversed(placements):
layer_index = get_layer_index(sub_widget.layer, 0)
# Combine regions with children to calculate the "virtual size"
if fixed:
widget_region = (
sub_region + sub_region_offset + placement_offset
)
else:
widget_region = (
sub_region + sub_region_offset + placement_scroll_offset
)

child_layer_order = first_layer_order + ordinal
widget_order = order + ((layer_index, z, child_layer_order),)

for (sub_widget, sub_region, widget_region, widget_order,
child_layer_order, child_clip) in self._place_children(
placements, len(arrange_result.placements),
container_region, widget.scroll_offset,
order, layer_order, sub_clip, no_clip, layers_to_index,
):
if widget._cover_widget is None:
add_widget( # noqa: F821 -- closure cleared only after traversal
sub_widget,
sub_region,
widget_region,
((1, 0, 0),) if overlay else widget_order,
widget_order,
child_layer_order,
no_clip if overlay else sub_clip,
child_clip,
visible,
arrange_result.scroll_spacing,
complete,
Expand Down Expand Up @@ -1478,6 +1455,28 @@ def add_widget(widget, virtual_region, region, order, layer_order, clip, visible
widgets -= invisible_widgets
return cast(CompositorMap, map), widgets

@staticmethod
def _place_children(placements, placement_count, container_region, scroll_offset,
order, layer_order, sub_clip, no_clip, layers):
"""Native placement projection, shared by scenes and detached paint.

Arrangement owns ordinals and local boxes. This projection owns fixed,
absolute, overlay, layer and clip meaning; detached documents don't
reconstruct those answers with a second placement algorithm.
"""
first_layer_order = layer_order - placement_count + 1
placement_offset = container_region.offset
placement_scroll_offset = placement_offset - scroll_offset
for ordinal, (region, offset, _, child, z, fixed, overlay, _) in reversed(placements):
child_region = region + offset + (
placement_offset if fixed else placement_scroll_offset
)
child_layer_order = first_layer_order + ordinal
child_order = order + ((layers.get(child.layer, 0), z, child_layer_order),)
yield (child, region, child_region,
((1, 0, 0),) if overlay else child_order, child_layer_order,
no_clip if overlay else sub_clip)

@property
def layers(self) -> list[tuple[Widget, MapGeometry]]:
"""Get widgets and geometry in layer order."""
Expand Down Expand Up @@ -1590,14 +1589,7 @@ def get_style_at(self, x: int, y: int) -> Style:

if not lines:
return Style.null()
end = 0

for segment in lines[0]:
end += segment.cell_length
if x < end:
return segment.style or Style.null()

return Style.null()
return lines[0].get_style_at(x)

def get_widget_and_offset_at(
self, x: int, y: int
Expand Down Expand Up @@ -1631,43 +1623,7 @@ def get_widget_and_offset_at(
visible_screen_stack.set(widget.app._background_screens)
line = widget.render_line(y)

end = 0
start = 0
offset_y: int | None = None
offset_x = 0
offset_x2 = 0

from rich.cells import get_character_cell_size

offset: Offset | None = None
for segment in line:
end += segment.cell_length
style = segment.style
if style is not None and style._meta is not None:
meta = style.meta
if "offset" in meta:
offset_x, offset_y = meta["offset"]
if offset_y is None:
continue
offset_x2 = offset_x + len(segment.text)

if x < end and x >= start:
segment_cell_length = 0
cell_cut = x - start
segment_offset = 0
for character in segment.text:
if segment_cell_length >= cell_cut:
break
segment_cell_length += get_character_cell_size(character)
segment_offset += 1

offset = Offset(offset_x + segment_offset, offset_y)
break
start = end

if offset is None and offset_y is not None:
offset = Offset(offset_x2, offset_y)
return widget, offset
return widget, line.get_content_offset(x)

def find_widget(self, widget: Widget) -> MapGeometry:
"""Get information regarding the relative position of a widget in the Compositor.
Expand Down
48 changes: 47 additions & 1 deletion src/textual/_extrema.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from __future__ import annotations

from fractions import Fraction
from typing import NamedTuple
from typing import TYPE_CHECKING, NamedTuple

from textual.geometry import Size

if TYPE_CHECKING:
from textual.css.styles import StylesBase


class Extrema(NamedTuple):
"""Specifies minimum and maximum dimensions."""
Expand Down Expand Up @@ -62,3 +65,46 @@ def apply_dimensions(self, width: int, height: int) -> Size:
int(self.apply_width(Fraction(width))),
int(self.apply_height(Fraction(height))),
)

@classmethod
def resolve(
cls, styles: StylesBase, container: Size, viewport: Size,
width_fraction: Fraction, height_fraction: Fraction,
) -> Extrema:
"""Resolve native min/max rules independently of scene custody."""
min_width: Fraction | None = None
max_width: Fraction | None = None
min_height: Fraction | None = None
max_height: Fraction | None = None

container -= styles.margin.totals
if styles.box_sizing == "border-box":
gutter_width, gutter_height = styles.gutter.totals
else:
gutter_width = gutter_height = 0

if styles.min_width is not None:
min_width = (
styles.min_width.resolve(container, viewport, width_fraction)
- gutter_width
)

if styles.max_width is not None:
max_width = (
styles.max_width.resolve(container, viewport, width_fraction)
- gutter_width
)
if styles.min_height is not None:
min_height = (
styles.min_height.resolve(container, viewport, height_fraction)
- gutter_height
)

if styles.max_height is not None:
max_height = (
styles.max_height.resolve(container, viewport, height_fraction)
- gutter_height
)

extrema = cls(min_width, max_width, min_height, max_height)
return extrema
7 changes: 0 additions & 7 deletions src/textual/_styles_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from textual import log
from textual._ansi_theme import DEFAULT_TERMINAL_THEME
from textual._border import get_box, render_border_label, render_row
from textual._context import active_app
from textual._opacity import _apply_opacity
from textual._segment_tools import apply_hatch, line_pad, line_trim, make_blank
from textual.color import TRANSPARENT, Color
Expand Down Expand Up @@ -347,12 +346,6 @@ def post(segments: Iterable[Segment]) -> Iterable[Segment]:
Returns:
New list of segments
"""
try:
app = active_app.get()
ansi_theme = app.ansi_theme
except LookupError:
ansi_theme = DEFAULT_TERMINAL_THEME

if styles.tint.a:
segments = Tint.process_segments(
segments, styles.tint, ansi_theme, background
Expand Down
Loading