Add packed layout Python prototype
This commit is contained in:
@@ -0,0 +1,401 @@
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
@dataclass(eq=False)
|
||||
class Box:
|
||||
rel_x: float
|
||||
rel_y: float
|
||||
width: float
|
||||
height: float
|
||||
item: "LayoutItem | None" = field(default=None, repr=False)
|
||||
collision_left: list["Box"] = field(default_factory=list, repr=False)
|
||||
collision_right: list["Box"] = field(default_factory=list, repr=False)
|
||||
|
||||
def get_left(self):
|
||||
return self.item.x + self.rel_x
|
||||
|
||||
def get_right(self):
|
||||
return self.get_left() + self.width
|
||||
|
||||
def get_top(self):
|
||||
return self.item.y + self.rel_y
|
||||
|
||||
def get_bottom(self):
|
||||
return self.get_top() + self.height
|
||||
|
||||
def is_in_collision_left_tree(self, box):
|
||||
if box in self.collision_left:
|
||||
return True
|
||||
return any(collision.is_in_collision_left_tree(box) for collision in self.collision_left)
|
||||
|
||||
def __repr__(self):
|
||||
if len(self.item.boxes) == 1:
|
||||
return self.item.name
|
||||
return f"{self.item.name}:{self.item.boxes.index(self)}"
|
||||
|
||||
|
||||
class LayoutItem:
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
kind,
|
||||
color,
|
||||
position,
|
||||
boxes,
|
||||
fallback=None,
|
||||
icon_group=None,
|
||||
icon_count=0,
|
||||
icon_width=40,
|
||||
icon_widths=None,
|
||||
icon_height=None,
|
||||
dot_width=40,
|
||||
allow_zero=False,
|
||||
allow_dot=False,
|
||||
direction=None,
|
||||
spacing=0,
|
||||
):
|
||||
self.name = name
|
||||
self.kind = kind
|
||||
self.color = color
|
||||
self.position = position if isinstance(position, str) else position[0]
|
||||
self.fallback = fallback if isinstance(position, str) else position[1]
|
||||
self.icon_group = icon_group
|
||||
self.icon_count = icon_count
|
||||
self.icon_width = icon_width
|
||||
self.icon_height = icon_height if icon_height is not None else icon_width
|
||||
self.icon_widths = self._expanded_icon_widths(icon_widths, icon_count, icon_width)
|
||||
self.dot_width = dot_width
|
||||
self.allow_zero = allow_zero
|
||||
self.allow_dot = allow_dot
|
||||
self.direction = direction
|
||||
self.spacing = spacing
|
||||
self.visible_icons = icon_count
|
||||
self.visible_icon_widths = self.icon_widths
|
||||
self.dot = False
|
||||
self.pool_remaining = icon_count
|
||||
self.pool_icon_widths = self.icon_widths
|
||||
self.width_limit = None
|
||||
self.x = 0
|
||||
self.y = 0
|
||||
self.boxes = boxes
|
||||
for box in self.boxes:
|
||||
box.item = self
|
||||
|
||||
if self.is_icon_item:
|
||||
self.width_limit = sum(self.icon_widths)
|
||||
self._apply_icon_width(self.width_limit)
|
||||
|
||||
@classmethod
|
||||
def fixed(cls, name, kind, color, position, width, height, xy=(0, 0), fallback=None):
|
||||
item = cls(name, kind, color, position, [Box(0, 0, width, height)], fallback=fallback)
|
||||
item.x, item.y = xy
|
||||
return item
|
||||
|
||||
@classmethod
|
||||
def multi_box(cls, name, kind, color, position, boxes, xy=(0, 0), fallback=None):
|
||||
item = cls(name, kind, color, position, boxes, fallback=fallback)
|
||||
item.x, item.y = xy
|
||||
return item
|
||||
|
||||
@classmethod
|
||||
def icon_strip(
|
||||
cls,
|
||||
name,
|
||||
kind,
|
||||
color,
|
||||
position,
|
||||
icon_group,
|
||||
icon_count,
|
||||
height,
|
||||
xy=(0, 0),
|
||||
icon_width=40,
|
||||
icon_widths=None,
|
||||
allow_zero=False,
|
||||
allow_dot=True,
|
||||
direction=None,
|
||||
spacing=0,
|
||||
):
|
||||
item = cls(
|
||||
name,
|
||||
kind,
|
||||
color,
|
||||
position,
|
||||
[Box(0, 0, 0, height)],
|
||||
icon_group=icon_group,
|
||||
icon_count=icon_count,
|
||||
icon_width=icon_width,
|
||||
icon_widths=icon_widths,
|
||||
icon_height=height,
|
||||
dot_width=height * 0.75,
|
||||
allow_zero=allow_zero,
|
||||
allow_dot=allow_dot,
|
||||
direction=direction,
|
||||
spacing=spacing,
|
||||
)
|
||||
item.x, item.y = xy
|
||||
return item
|
||||
|
||||
@property
|
||||
def is_icon_item(self):
|
||||
return self.icon_group is not None
|
||||
|
||||
def get_width(self):
|
||||
return self.get_right() - self.get_left()
|
||||
|
||||
def get_left(self):
|
||||
return min(box.get_left() for box in self.boxes)
|
||||
|
||||
def get_right(self):
|
||||
return max(box.get_right() for box in self.boxes)
|
||||
|
||||
def get_top(self):
|
||||
return min(box.get_top() for box in self.boxes)
|
||||
|
||||
def get_bottom(self):
|
||||
return max(box.get_bottom() for box in self.boxes)
|
||||
|
||||
def set_left(self, x):
|
||||
self.x += x - self.get_left()
|
||||
|
||||
def set_right(self, x):
|
||||
self.x += x - self.get_right()
|
||||
|
||||
def set_top(self, y):
|
||||
self.y += y - self.get_top()
|
||||
|
||||
def shift_x(self, offset):
|
||||
self.x += offset
|
||||
|
||||
def collision_amount_left(self, items=None):
|
||||
boxes = self._boxes_for_items(items)
|
||||
amount = 0
|
||||
for box in self.boxes:
|
||||
for collision in box.collision_left:
|
||||
if boxes is not None and collision not in boxes:
|
||||
continue
|
||||
for blocker, blocker_item in self._visible_blockers(collision, "left", boxes):
|
||||
amount = max(
|
||||
amount,
|
||||
blocker + blocker_item.spacing_to(self) - box.get_left(),
|
||||
)
|
||||
return amount
|
||||
|
||||
def collision_amount_right(self, items=None):
|
||||
boxes = self._boxes_for_items(items)
|
||||
amount = 0
|
||||
for box in self.boxes:
|
||||
for collision in box.collision_right:
|
||||
if boxes is not None and collision not in boxes:
|
||||
continue
|
||||
for blocker, blocker_item in self._visible_blockers(collision, "right", boxes):
|
||||
amount = max(
|
||||
amount,
|
||||
box.get_right() + self.spacing_to(blocker_item) - blocker,
|
||||
)
|
||||
return amount
|
||||
|
||||
def spacing_to(self, other):
|
||||
if self.get_width() <= 0 or other.get_width() <= 0:
|
||||
return 0
|
||||
if self.icon_group is not None and self.icon_group == other.icon_group:
|
||||
return 0
|
||||
return self.spacing
|
||||
|
||||
def get_collisionleft_items(self):
|
||||
return self._collision_items("left")
|
||||
|
||||
def get_collisionright_items(self):
|
||||
return self._collision_items("right")
|
||||
|
||||
def min_width(self):
|
||||
if not self.is_icon_item:
|
||||
if self.kind == "Chip":
|
||||
return 0
|
||||
return self.get_width()
|
||||
if self.icon_count <= 0 or self.pool_remaining <= 0:
|
||||
return 0
|
||||
if self.allow_zero:
|
||||
return 0
|
||||
return self._dot_width(visible_icons=0)
|
||||
|
||||
def can_shrink_by(self):
|
||||
return max(0, self.get_width() - self.min_width())
|
||||
|
||||
def truncate_at_least(self, amount):
|
||||
amount = max(0, min(amount, self.can_shrink_by()))
|
||||
old_width = self.get_width()
|
||||
if self.is_icon_item:
|
||||
self.width_limit = max(0, self.width_limit - amount)
|
||||
self.recompute_icon_state()
|
||||
self.width_limit = self.get_width()
|
||||
else:
|
||||
self._set_primary_width(old_width - amount)
|
||||
return old_width - self.get_width()
|
||||
|
||||
def set_pool_remaining(self, remaining):
|
||||
self.pool_remaining = max(0, remaining)
|
||||
self.recompute_icon_state()
|
||||
|
||||
def recompute_icon_state(self):
|
||||
if not self.is_icon_item:
|
||||
return
|
||||
|
||||
max_width = min(self.width_limit, sum(self.pool_icon_widths))
|
||||
width, visible, dot = self._best_icon_state(max_width)
|
||||
self.visible_icons = visible
|
||||
self.visible_icon_widths = self.pool_icon_widths[:visible]
|
||||
self.dot = dot
|
||||
self._apply_icon_width(width)
|
||||
|
||||
def set_pool_icons(self, widths):
|
||||
self.width_limit = min(self.width_limit, self.get_width())
|
||||
self.pool_icon_widths = list(widths)
|
||||
self.pool_remaining = len(self.pool_icon_widths)
|
||||
self.recompute_icon_state()
|
||||
self.width_limit = self.get_width()
|
||||
|
||||
def rendername(self):
|
||||
text = f"{self.name}"
|
||||
if self.is_icon_item:
|
||||
text += f"\n{self.visible_icons}/{self.icon_count} icons" + (" *" if self.dot else "")
|
||||
text += f"\n{self.get_width()}"
|
||||
return text
|
||||
|
||||
def render_segments(self):
|
||||
if not self.is_icon_item:
|
||||
return []
|
||||
segments = [("icon", width) for width in self.visible_icon_widths]
|
||||
if self.dot:
|
||||
dot = ("dot", self._dot_width())
|
||||
if self._dot_side() == "left":
|
||||
segments.insert(0, dot)
|
||||
else:
|
||||
segments.append(dot)
|
||||
return segments
|
||||
|
||||
def _best_icon_state(self, max_width):
|
||||
if self.pool_remaining <= 0:
|
||||
return 0, 0, False
|
||||
|
||||
max_icons = len(self.pool_icon_widths)
|
||||
prefix_widths = [0]
|
||||
for width in self.pool_icon_widths:
|
||||
prefix_widths.append(prefix_widths[-1] + width)
|
||||
|
||||
best = None
|
||||
for visible in range(max_icons, -1, -1):
|
||||
hidden = self.pool_remaining - visible
|
||||
dot = self.allow_dot and hidden > 0
|
||||
icon_width = prefix_widths[visible]
|
||||
if visible == 0 and not dot and not self.allow_zero:
|
||||
continue
|
||||
width = None
|
||||
if visible == 0 and dot and self.allow_zero:
|
||||
width = self._dot_width()
|
||||
elif visible > 0 or dot:
|
||||
width = icon_width + (self._dot_width() if dot else 0)
|
||||
elif self.allow_zero:
|
||||
width = 0
|
||||
if width is not None and width <= max_width:
|
||||
if best is None or width > best[0]:
|
||||
best = (width, visible, dot)
|
||||
if best is not None:
|
||||
return best
|
||||
return 0, 0, False
|
||||
|
||||
def _dot_width(self, visible_icons=None):
|
||||
visible_icons = self.visible_icons if visible_icons is None else visible_icons
|
||||
crop = 0
|
||||
dot_side = self._dot_side()
|
||||
if dot_side == "left" and all(not box.collision_left for box in self.boxes):
|
||||
crop += self.dot_width / 3
|
||||
if dot_side == "right" and all(not box.collision_right for box in self.boxes):
|
||||
crop += self.dot_width / 3
|
||||
if visible_icons == 0:
|
||||
if dot_side != "left" and all(not box.collision_left for box in self.boxes):
|
||||
crop += self.dot_width / 3
|
||||
if dot_side != "right" and all(not box.collision_right for box in self.boxes):
|
||||
crop += self.dot_width / 3
|
||||
return max(0, self.dot_width - crop)
|
||||
|
||||
def _dot_side(self):
|
||||
if self.direction in ("left", "right"):
|
||||
return self.direction
|
||||
if self.position == "right":
|
||||
return "left"
|
||||
return "right"
|
||||
|
||||
def _apply_icon_width(self, width):
|
||||
self._set_primary_width(width)
|
||||
|
||||
def _set_primary_width(self, width):
|
||||
self.boxes[0].width = max(0, width)
|
||||
|
||||
@staticmethod
|
||||
def _expanded_icon_widths(widths, count, fallback):
|
||||
if count <= 0:
|
||||
return []
|
||||
if widths is None:
|
||||
return [fallback] * count
|
||||
source = list(widths)
|
||||
if not source:
|
||||
source = [fallback]
|
||||
if len(source) < count:
|
||||
source.extend([source[-1]] * (count - len(source)))
|
||||
return source[:count]
|
||||
|
||||
def _boxes_for_items(self, items):
|
||||
if items is None:
|
||||
return None
|
||||
return {box for item in items for box in item.boxes}
|
||||
|
||||
def _visible_blockers(self, box, side, allowed_boxes=None):
|
||||
seen = set()
|
||||
|
||||
def visit(current):
|
||||
if current in seen:
|
||||
return
|
||||
seen.add(current)
|
||||
if allowed_boxes is not None and current not in allowed_boxes:
|
||||
return
|
||||
if current.item.get_width() > 0:
|
||||
if side == "left":
|
||||
yield current.get_right(), current.item
|
||||
else:
|
||||
yield current.get_left(), current.item
|
||||
return
|
||||
|
||||
collisions = current.collision_left if side == "left" else current.collision_right
|
||||
found = False
|
||||
for collision in collisions:
|
||||
for blocker in visit(collision):
|
||||
found = True
|
||||
yield blocker
|
||||
if not found:
|
||||
if side == "left":
|
||||
yield current.get_right(), current.item
|
||||
else:
|
||||
yield current.get_left(), current.item
|
||||
|
||||
yield from visit(box)
|
||||
|
||||
def _collision_items(self, side):
|
||||
seen = set()
|
||||
result = []
|
||||
|
||||
def visit(box):
|
||||
if box in seen:
|
||||
return
|
||||
seen.add(box)
|
||||
if box.item not in result:
|
||||
result.append(box.item)
|
||||
collisions = box.collision_left if side == "left" else box.collision_right
|
||||
for collision in collisions:
|
||||
visit(collision)
|
||||
|
||||
for box in self.boxes:
|
||||
visit(box)
|
||||
return result
|
||||
|
||||
def __repr__(self):
|
||||
return self.name
|
||||
Reference in New Issue
Block a user