"""SplitContainer: two-panel split with draggable divider."""
import logging
from ..descriptors import Property
from ..input.controls import Pointer
from ..input.enums import CursorShape, PointerPhase
from ..input.events import PointerEvent
from ..math.types import Vec2
from ..signals import Signal
from .containers import Container # also provides Container._place
from .core import Control, ThemeColour
from .enums import PointerFilter
log = logging.getLogger(__name__)
__all__ = ["SplitContainer"]
_DIVIDER_GRAB_MARGIN = 4.0
[docs]
class SplitContainer(Container):
"""Two-panel split container with a draggable divider.
When vertical=True (default), panels are arranged side by side (left | right).
When vertical=False, panels are stacked top / bottom.
Only the first two children are laid out; additional children are ignored.
Example:
split = SplitContainer(vertical=True, split_ratio=0.3)
split.add_child(Panel()) # left panel (30%)
split.add_child(Panel()) # right panel (70%)
split.split_changed.connect(lambda r: print(f"ratio: {r}"))
"""
pointer_filter = Property(PointerFilter.STOP, hint="How this control takes part in the pointer hit test")
split_ratio = Property(0.5, range=(0.0, 1.0), hint="Split position (0-1)")
size_x = Property(400.0, range=(0, 10000), hint="Control width", on_change="_on_size_changed")
size_y = Property(300.0, range=(0, 10000), hint="Control height", on_change="_on_size_changed")
divider_colour = ThemeColour("divider")
divider_hover_colour = ThemeColour("divider_hover")
def __init__(self, vertical: bool = True, split_ratio: float = 0.5, **kwargs):
super().__init__(**kwargs)
self.vertical = vertical
self.split_ratio = max(0.05, min(0.95, split_ratio))
self.divider_width = 6.0
self._dragging = False
self._hover_divider = False
self.split_changed = Signal()
def _get_panels(self) -> tuple[Control | None, Control | None]:
"""Return the first two Control children."""
controls = [c for c in self.children if isinstance(c, Control)]
first = controls[0] if len(controls) > 0 else None
second = controls[1] if len(controls) > 1 else None
return first, second
def _divider_rect(self) -> tuple[float, float, float, float]:
"""Return (x, y, w, h) of the divider in local space."""
x, y, w, h = self._local_rect()
half = round(self.divider_width / 2)
if self.vertical:
dx = x + round(w * self.split_ratio) - half
return (dx, y, self.divider_width, h)
else:
dy = y + round(h * self.split_ratio) - half
return (x, dy, w, self.divider_width)
def _point_on_divider(self, pos) -> bool:
"""Check if a point is within the divider grab area."""
dx, dy, dw, dh = self._divider_rect()
try:
px, py = self.to_local(pos)
except ValueError:
return False # scaled to zero: nothing to hit
return (
dx - _DIVIDER_GRAB_MARGIN <= px <= dx + dw + _DIVIDER_GRAB_MARGIN
and dy - _DIVIDER_GRAB_MARGIN <= py <= dy + dh + _DIVIDER_GRAB_MARGIN
)
[docs]
def get_minimum_size(self) -> Vec2:
first, second = self._get_panels()
m1 = first._combined_minimum_size() if first else Vec2(0, 0)
m2 = second._combined_minimum_size() if second else Vec2(0, 0)
if self.vertical:
w = m1.x + m2.x + self.divider_width
h = max(m1.y, m2.y)
else:
w = max(m1.x, m2.x)
h = m1.y + m2.y + self.divider_width
floor_x, floor_y = self.size_floor()
return Vec2(max(floor_x, w), max(floor_y, h))
def _update_layout(self):
first, second = self._get_panels()
_, _, w, h = self.get_rect()
half = round(self.divider_width / 2)
place = Container._place
if self.vertical:
split_px = round(w * self.split_ratio)
rh = round(h)
if first:
place(first, 0, 0, max(0, split_px - half), rh)
if second:
place(second, split_px + half, 0, max(0, w - split_px - half), rh)
else:
split_px = round(h * self.split_ratio)
rw = round(w)
if first:
place(first, 0, 0, rw, max(0, split_px - half))
if second:
place(second, 0, split_px + half, rw, max(0, h - split_px - half))
[docs]
def on_update(self, dt: float):
super().on_update(dt)
# When hovering the divider but no longer receiving gui_input (mouse moved
# onto a child panel), poll the mouse position to detect exit.
if self._hover_divider and not self._dragging:
from ..input.state import Input
mx, my = Input.mouse_position
if not self._point_on_divider((mx, my)):
self._hover_divider = False
self._update_cursor(False)
def _update_cursor(self, resize_cursor: bool):
"""Show resize arrows while the pointer is on the divider, and the parent's shape otherwise."""
if resize_cursor:
self.cursor_shape = CursorShape.RESIZE_EW if self.vertical else CursorShape.RESIZE_NS
else:
self.cursor_shape = None
[docs]
def on_draw(self, renderer):
# Only draw the divider: children are drawn by _draw_recursive tree traversal
dx, dy, dw, dh = self._divider_rect()
colour = self.divider_hover_colour if (self._hover_divider or self._dragging) else self.divider_colour
renderer.rect((dx, dy), (dw, dh), fill=colour)