402 lines
13 KiB
Python
402 lines
13 KiB
Python
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
|