Files
statusbartweak/python/layout_item.py
T

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