from dataclasses import dataclass from layout_item import Box, LayoutItem SCREEN_WIDTH = 1200 SCREEN_HEIGHT = 160 @dataclass class Scenario: cutout: bool = True camera_x: int = 770 camera_width: int = 100 clock_pos: str = "left" chip_pos: tuple[str, str] = ("middle", "right") clock_vertical: int = 20 chip_vertical: int = 90 notifs_count: int = 2 status_count: int = 2 dummy_count: int = 3 notifs_pos: tuple[str | tuple[str, str], ...] = ("left",) status_pos: tuple[str | tuple[str, str], ...] = ("right",) dummy_pos: tuple[str | tuple[str, str], ...] = (("middle", "left"),) notifs_vertical: tuple[int, ...] = (0, 0, 0) status_vertical: tuple[int, ...] = (0, 100) dummy_vertical: tuple[int, ...] = (50, 0, 20) notifs_icons: int = 20 status_icons: int = 20 notifs_icon_width: int = 50 status_icon_widths: tuple[int, ...] = (30, 30, 30, 30) status_icon_height: int = 30 container_spacing: int = 10 order: tuple[str, ...] = ("Clock", "Chip", "Status", "Notifs", "Dummy") shrink_order: tuple[str, ...] = ("Notifs", "Status", "Chip") @dataclass class PathState: boxes: list[Box] gaps: float = 0 def added(self, box): gap = self.gaps previous = self.boxes[-1] if previous.item is not box.item: actual_gap = box.get_left() - previous.get_right() required_gap = previous.item.spacing_to(box.item) gap += max(0, actual_gap - required_gap) return PathState(self.boxes + [box], gap) def get_right(self): return self.boxes[-1].get_right() def position_containers(scenario=None): scenario = scenario or Scenario() # 1. Build the test items from explicit scenario settings. items = make_test_items(scenario) regions = make_regions(scenario) # 2. Assign each item to a cutout-separated region and build collision edges. place_into_regions(regions, items) for region in regions: collision_tree(region["items"]) print_regions(regions) shrink_priority = [ item for kind in scenario.shrink_order for item in items if item.kind == kind ] # 3. Build constraints for both regions first. Shared icon pools can span # regions, so truncation must make decisions with all paths visible at once. constraints = [] for region in regions: region_shrink_priority = [item for item in shrink_priority if item in region["items"]] constraints.extend(collect_constraints(region, region_shrink_priority)) # 4. Run one shared truncation/allocation pass over both regions. if constraints: truncate(constraints, shrink_priority, items) # 5. Restack the final truncated items into their selected positions. for region in regions: final_stack(region) if scenario.cutout: return items + [make_camera_item(scenario)] return items def make_test_items(scenario): items = [] clock = LayoutItem.multi_box( "Clock", "Clock", (120, 180, 255), scenario.clock_pos, [ Box(0, 0, 240, 40), Box(80, -20, 30, 30), ], xy=(0, scenario.clock_vertical), ) clock.spacing = scenario.container_spacing items.append(clock) chip = LayoutItem.fixed( "Chip", "Chip", (220, 140, 255), scenario.chip_pos, 400, 40, xy=(0, scenario.chip_vertical), ) chip.spacing = scenario.container_spacing items.append(chip) for i in range(scenario.notifs_count): items.append(LayoutItem.icon_strip( f"Notifs{i}", "Notifs", (120, 255, 160), repeated(scenario.notifs_pos, i), "notifications", scenario.notifs_icons, scenario.notifs_icon_width, xy=(0, repeated(scenario.notifs_vertical, i)), icon_width=scenario.notifs_icon_width, spacing=scenario.container_spacing, )) status_widths = repeated_widths(scenario.status_icon_widths, scenario.status_icons) for i in range(scenario.status_count): items.append(LayoutItem.icon_strip( f"Status{i}", "Status", (120, 255, 160), repeated(scenario.status_pos, i), "status", scenario.status_icons, scenario.status_icon_height, xy=(0, repeated(scenario.status_vertical, i)), icon_width=scenario.status_icon_height, icon_widths=status_widths, spacing=scenario.container_spacing, )) for i in range(scenario.dummy_count): dummy = LayoutItem.fixed( f"Dummy{i}", "Dummy", (120, 255, 160), repeated(scenario.dummy_pos, i), 100, 30, xy=(0, repeated(scenario.dummy_vertical, i)), ) dummy.spacing = scenario.container_spacing items.append(dummy) items = sort_items_by_kind_order(items, scenario.order) configure_icon_groups(items) return items def repeated(values, index): if not isinstance(values, tuple) or is_single_position(values): return values if len(values) == 0: return None return values[min(index, len(values) - 1)] def is_single_position(value): return ( isinstance(value, tuple) and len(value) == 2 and value[0] == "middle" and value[1] in ("left", "right") ) def repeated_widths(values, count): if count <= 0: return [] widths = list(values) if not widths: widths = [40] if len(widths) < count: widths.extend([widths[-1]] * (count - len(widths))) return widths[:count] def configure_icon_groups(items): groups = {} for item in items: if item.icon_group is not None: groups.setdefault(item.icon_group, []).append(item) for group_items in groups.values(): if not group_items: continue group_items[0].allow_zero = False group_items[0].allow_dot = True for item in group_items[1:]: item.allow_zero = True item.allow_dot = False def make_regions(scenario): if not scenario.cutout: return [{"leftwall": 0, "rightwall": SCREEN_WIDTH, "items": []}] camera_left = scenario.camera_x camera_right = scenario.camera_x + scenario.camera_width return [ {"leftwall": 0, "rightwall": camera_left, "items": []}, {"leftwall": camera_right, "rightwall": SCREEN_WIDTH, "items": []}, ] def make_camera_item(scenario): return LayoutItem.fixed( "Camera", "Camera", (0, 0, 0), "camera", scenario.camera_width, SCREEN_HEIGHT, xy=(scenario.camera_x, 0), ) def collect_constraints(region, shrink_priority): stackleft(region["items"], region["leftwall"]) paths = get_paths(region["items"], region["rightwall"]) if not paths: return [] wall = region["rightwall"] print("Does not fit!") print(f" Wall is at {wall}") path_constraints = [] best_path_by_constraint_key = {} for path in paths: shrinkables = unique_items( box.item for box in path.boxes if box.item in shrink_priority ) items_on_path = unique_items(box.item for box in path.boxes) effective_right = path.get_right() - path.gaps key = ( path.boxes[-1], tuple(shrinkables), tuple(items_on_path), ) if key not in best_path_by_constraint_key: best_path_by_constraint_key[key] = len(path_constraints) path_constraints.append({ "path": path, "end_box": path.boxes[-1], "shrinkables": shrinkables, "effective_right": effective_right, "items_on_path": items_on_path, }) continue previous = path_constraints[best_path_by_constraint_key[key]] if effective_right > previous["effective_right"]: previous.update({ "path": path, "effective_right": effective_right, }) print(" Paths:") for constraint in path_constraints: print(f" {describe_path(constraint['path'])}") # Convert unique overflowing paths into live constraints immediately. Most # scenarios do not need relaxed walls, so avoid a second pass unless one is # discovered. final_constraints = [] constraints_by_end_box = {} new_wall_by_end_box = {} for path_constraint in path_constraints: shrinkables = path_constraint["shrinkables"] effective_right = path_constraint["effective_right"] total_capacity = sum(truncation_phase_shrink_capacity(item) for item in shrinkables) minimum_possible_right = effective_right - total_capacity if minimum_possible_right > wall: current = new_wall_by_end_box.get(path_constraint["end_box"], wall) if minimum_possible_right > current: new_wall_by_end_box[path_constraint["end_box"]] = minimum_possible_right constraint = { "end_box": path_constraint["end_box"], "shrinkables": shrinkables, "effective_right": effective_right, "required": effective_right - wall, "items_on_path": path_constraint["items_on_path"], } if constraint["required"] > 0: final_constraints.append(constraint) constraints_by_end_box.setdefault(path_constraint["end_box"], []).append(constraint) print(" Constraint:") print(f" end: {path_constraint['end_box']}") print(f" shrinkables: {shrinkables}") print(f" effective right: {effective_right}") print(f" capacity: {total_capacity}") # Relax only impossible paths. If this map is empty, final_constraints is # already complete from the single pass above. for end_box, relaxed_wall in new_wall_by_end_box.items(): for constraint in constraints_by_end_box.get(end_box, []): constraint["required"] = constraint["effective_right"] - relaxed_wall final_constraints = [ constraint for constraint in final_constraints if constraint["required"] > 0 ] for constraint in final_constraints: print(" Final constraint:") print(f" required: {constraint['required']}") print(f" shrinkables: {constraint['shrinkables']}") return final_constraints def truncate(final_constraints, shrink_priority, all_items): # The scenario order is an absorption order: earlier kinds are more willing # to absorb shrinkage. Within a kind, lower-numbered copies are more # important and are therefore processed before later copies. # # For each item, ask how much it must shrink if every still-unprocessed, # more-willing item on the same path shrinks as far as it can. Already # processed shrink is fixed and counted exactly. # Icon groups are special: when one item in a shared pool is processed, immediately # reallocate that pool so sibling containers stop reserving space for icons that # have already been claimed by earlier containers. processed = set() execution_order = truncation_execution_order(shrink_priority) absorption_order = truncation_absorption_order(shrink_priority) absorption_index = {item: index for index, item in enumerate(absorption_order)} kind_absorption_index = kind_order_index(shrink_priority) icon_groups = grouped_icon_items(all_items) remaining_icon_widths = { group: list(group_items[0].icon_widths) for group, group_items in icon_groups.items() } print(" Truncation order:") print(f" shrink absorption order: {absorption_order}") print(f" execution least-willing kind first, preserving copy order: {execution_order}") for index, item in enumerate(execution_order): print(f" Considering {item}:") needed_for_item = 0 for constraint in final_constraints: if constraint["required"] <= 0: continue shrinkables = constraint["shrinkables"] if item not in shrinkables: continue assumed_absorbers = [ other for other in shrinkables if other not in processed and other is not item and can_absorb_for(item, other, kind_absorption_index, absorption_index) ] assumed_absorber_capacity = sum( contextual_shrink_capacity(item, other) for other in assumed_absorbers ) needed = ( constraint["required"] - assumed_absorber_capacity ) needed_for_item = max(needed_for_item, needed) print(" Live constraint:") print(f" required: {constraint['required']}") print(f" shrinkables: {shrinkables}") print(f" assumed absorbers: {assumed_absorbers}") print(f" assumed absorber capacity: {assumed_absorber_capacity}") print(f" needed from this item: {needed}") amount = min(max(0, needed_for_item), contextual_shrink_capacity(item, item)) old_width = item.get_width() if item.is_icon_item: direct_actual = truncate_icon_item_without_dot(item, amount) else: direct_actual = item.truncate_at_least(amount) actual = direct_actual if direct_actual != 0: adjust_constraints_for_item_width_delta(final_constraints, item, direct_actual) if item.is_icon_item: remaining_icon_widths[item.icon_group] = allocate_current_icon_item( item, icon_groups[item.icon_group], remaining_icon_widths[item.icon_group], constraints=final_constraints, ) actual = old_width - item.get_width() print(" Truncation:") print(f" item: {item}") print(f" requested: {amount}") print(f" actual: {actual}") print(f" final width: {item.get_width()}") processed.add(item) next_item = execution_order[index + 1] if index + 1 < len(execution_order) else None if item.is_icon_item and (next_item is None or next_item.kind != item.kind): finish_icon_group( item.icon_group, icon_groups[item.icon_group], remaining_icon_widths[item.icon_group], constraints=final_constraints, ) def can_absorb_for(current, other, kind_absorption_index, absorption_index): current_kind = kind_absorption_index.get(current.kind) other_kind = kind_absorption_index.get(other.kind) if current_kind is None or other_kind is None: return False if other_kind < current_kind: return True if other_kind > current_kind: return False return absorption_index[other] < absorption_index[current] def contextual_shrink_capacity(current, other): if current.icon_group is not None and current.icon_group == other.icon_group: return other.get_width() return other.can_shrink_by() def truncation_phase_shrink_capacity(item): if item.icon_group is not None: return item.get_width() return item.can_shrink_by() def truncate_icon_item_without_dot(item, amount): old_allow_zero = item.allow_zero old_allow_dot = item.allow_dot item.allow_zero = True item.allow_dot = False try: return item.truncate_at_least(amount) finally: item.allow_zero = old_allow_zero item.allow_dot = old_allow_dot def truncation_execution_order(shrink_priority): by_kind = {} for item in shrink_priority: by_kind.setdefault(item.kind, []).append(item) result = [] for kind in reversed(list(by_kind.keys())): result.extend(by_kind[kind]) return result def truncation_absorption_order(shrink_priority): by_kind = {} for item in shrink_priority: by_kind.setdefault(item.kind, []).append(item) result = [] for items in by_kind.values(): result.extend(reversed(items)) return result def grouped_icon_items(items): grouped_items = {} for item in items: if item.icon_group is not None: grouped_items.setdefault(item.icon_group, []).append(item) for group_items in grouped_items.values(): group_items.sort(key=item_copy_index) return grouped_items def allocate_current_icon_item(item, group_items, remaining_widths, constraints): print(f"Icon distribution while truncating {item}:") print(f" Group {item.icon_group}:") print(f" remaining before: {remaining_widths}") old_widths = {group_item: group_item.get_width() for group_item in group_items} set_icon_item_pool(item, remaining_widths) remaining_after_item = remaining_widths[item.visible_icons:] item_index = group_items.index(item) for later_item in group_items[item_index + 1:]: set_icon_item_pool(later_item, remaining_after_item) for group_item in group_items: delta = old_widths[group_item] - group_item.get_width() print(f" Item {group_item}:") print(f" available before: {group_item.pool_icon_widths}") print(f" visible widths: {group_item.visible_icon_widths}") print(f" dot: {group_item.dot}") print(f" width: {old_widths[group_item]} -> {group_item.get_width()}") print(f" remaining after item: {remaining_after_item}") if delta != 0: adjust_constraints_for_item_width_delta(constraints, group_item, delta) return remaining_after_item def finish_icon_group(group, group_items, remaining_widths, constraints): print(f"Final icon distribution while finishing {group_items[0].kind}:") print(f" Group {group}:") print(f" remaining before dot: {remaining_widths}") old_widths = {item: item.get_width() for item in group_items} if remaining_widths: growth_rooms = { item: expansion_room_for_constraints(item, constraints) for item in group_items } place_final_dot(group_items, growth_rooms) for item in group_items: delta = old_widths[item] - item.get_width() print(f" Item {item}:") print(f" available before: {item.pool_icon_widths}") print(f" visible widths: {item.visible_icon_widths}") print(f" dot: {item.dot}") print(f" width: {old_widths[item]} -> {item.get_width()}") print(f" remaining after group: {remaining_widths}") if delta != 0: adjust_constraints_for_item_width_delta(constraints, item, delta) def set_icon_item_pool(item, widths): old_allow_zero = item.allow_zero old_allow_dot = item.allow_dot item.allow_zero = True item.allow_dot = False try: item.width_limit = item.get_width() item.set_pool_icons(widths) finally: item.allow_zero = old_allow_zero item.allow_dot = old_allow_dot def place_final_dot(group_items, growth_rooms): print(" Final dot placement:") capacities = { item: item.get_width() + growth_rooms.get(item, 0) for item in group_items } while True: last_icons = last_item_with_icons(group_items) if last_icons is None: last_icons = 0 for item in group_items[last_icons:]: dot_width = dot_width_for_final_placement(group_items, item) capacity = capacities[item] visible_widths = list(item.visible_icon_widths) print(f" trying {item}: capacity={capacity}, growth={growth_rooms.get(item, 0)}, own_limit={item.width_limit}, icons={visible_widths}, dot={dot_width}") if sum(visible_widths) + dot_width <= capacity: item.visible_icon_widths = visible_widths item.visible_icons = len(visible_widths) item.dot = True item._apply_icon_width(sum(visible_widths) + dot_width) item.width_limit = item.get_width() print(f" placed dot in {item}") return True item = group_items[last_icons] if not item.visible_icon_widths: break removed = item.visible_icon_widths.pop() item.visible_icons = len(item.visible_icon_widths) item._apply_icon_width(sum(item.visible_icon_widths)) item.width_limit = item.get_width() print(f" sacrificed icon width {removed} from {item}") print(" no item can fit the dot") return False def dot_width_for_final_placement(group_items, item): dot_width = item._dot_width() dot_side = item._dot_side() index = group_items.index(item) if dot_side == "right" and all(other.get_width() <= 0 for other in group_items[index + 1:]): return min(dot_width, item.dot_width - item.dot_width / 3) if dot_side == "left" and all(other.get_width() <= 0 for other in group_items[:index]): return min(dot_width, item.dot_width - item.dot_width / 3) return dot_width def last_item_with_icons(group_items): for index in range(len(group_items) - 1, -1, -1): if group_items[index].visible_icons > 0: return index return None def expansion_room_for_constraints(item, constraints): relevant = [ constraint for constraint in constraints if item in constraint["items_on_path"] ] if not relevant: return float("inf") return max(0, min(-constraint["required"] for constraint in relevant)) def adjust_constraints_for_item_width_delta(constraints, item, delta): for constraint in constraints: if item in constraint["items_on_path"]: constraint["required"] -= delta def live_constraints(constraints): return [constraint for constraint in constraints if constraint["required"] > 0] def item_copy_index(item): suffix = "" for char in reversed(item.name): if not char.isdigit(): break suffix = char + suffix return int(suffix) if suffix else 0 def get_paths(items, wall): boxes = [box for item in items for box in item.boxes] paths = [] def can_start(box): if box.collision_left: return False if len(box.item.boxes) > 1 and box.get_left() != box.item.get_left(): return False return True def can_end(box): if box.collision_right: return False if box.get_right() <= wall: return False if len(box.item.boxes) > 1 and box.get_right() != box.item.get_right(): return False return True def can_item_hop(path, box): return len(box.item.boxes) > 1 and sum(1 for b in path.boxes if b.item is box.item) < 2 def iter_next_boxes(path): box = path.boxes[-1] yield from box.collision_right if can_item_hop(path, box): for other in box.item.boxes: if other is not box: yield other def dfs(path): if can_end(path.boxes[-1]): paths.append(path) seen = set(path.boxes) for next_box in iter_next_boxes(path): if next_box in seen: continue dfs(path.added(next_box)) for box in boxes: if can_start(box): dfs(PathState([box])) return paths def describe_path(path): return ( " -> ".join(str(box) for box in path.boxes) + f" | right={path.get_right()}" + f" | collapsible_gap={path.gaps}" ) def final_stack(region): stackleft([item for item in region["items"] if item.position == "left"], region["leftwall"]) stackright([item for item in region["items"] if item.position == "right"], region["rightwall"]) items = [item for item in region["items"] if item.position == "middle"] middle = SCREEN_WIDTH / 2 for item in items: print(f"Positioning {item} in the middle.") item.set_left(middle - item.get_width() / 2) shift_amount = item.collision_amount_left(items) if shift_amount != 0: print(f" It collides. Push it to the right by {shift_amount}") item.shift_x(shift_amount) collision_tree_items = [collision for collision in item.get_collisionleft_items() if collision in items] left = min(collision.get_left() for collision in collision_tree_items) right = item.get_right() current_mid = (left + right) / 2 print(f" Then, push the whole left collision tree left by {current_mid - middle}") for collision in collision_tree_items: collision.shift_x(middle - current_mid) for item in items: print(f"Checking {item} for collisions left.") amount_left = max(item.collision_amount_left(), region["leftwall"] - item.get_left()) if amount_left > 0: print(f" It collides on the left. Push its right collision tree to the right by {amount_left}") collision_tree_items = [collision for collision in item.get_collisionright_items() if collision in items] for collision in collision_tree_items: print(f" shifting {collision} by {amount_left}") collision.shift_x(amount_left) for item in reversed(items): print(f"Checking {item} for collisions right.") amount_right = max(item.collision_amount_right(), item.get_right() - region["rightwall"]) if amount_right > 0: print(f" It collides on the right. Push its left collision tree to the left by {amount_right}") collision_tree_items = [collision for collision in item.get_collisionleft_items() if collision in items] for collision in collision_tree_items: collision.shift_x(-amount_right) def stackleft(items, against_wall): for item in items: item.set_left(against_wall) item.shift_x(item.collision_amount_left()) def stackright(items, against_wall): for item in reversed(items): item.set_right(against_wall) item.shift_x(-item.collision_amount_right()) def place_into_regions(regions, items): left_region = 0 right_region = len(regions) - 1 forceleft = False forceright = False middle = SCREEN_WIDTH / 2 if middle < regions[left_region]["rightwall"]: midregion = left_region elif regions[right_region]["leftwall"] < middle: midregion = right_region else: midregion = None forceleft = True forceright = True for item in items: if item.position == "middle" and item.fallback == "left": print(f"Placing {item} in middle/left") if forceleft: print(" Force is in effect. Position it in the left region.") regions[left_region]["items"].insert(0, item) elif overlaps_cutout(item, middle, regions): print(" Overlap with cutout. Force.") regions[left_region]["items"].insert(0, item) forceleft = True else: print(f" Middle in region {midregion}") regions[midregion]["items"].insert(0, item) elif item.position == "middle" and item.fallback == "right": print(f"Placing {item} in middle/right") if forceright: print(" Force is in effect. Position it in the right region.") regions[right_region]["items"].append(item) elif overlaps_cutout(item, middle, regions): print(" Overlap with cutout. Force.") regions[right_region]["items"].append(item) forceright = True else: print(f" Middle in region {midregion}") regions[midregion]["items"].append(item) for item in reversed(items): if item.position == "left": regions[left_region]["items"].insert(0, item) elif item.position == "right": regions[right_region]["items"].append(item) def overlaps_cutout(item, middle, regions): if len(regions) < 2: return False item_left = middle - item.get_width() / 2 item_right = middle + item.get_width() / 2 return max(item_left, regions[0]["rightwall"]) < min(item_right, regions[1]["leftwall"]) def collision_tree(items): for item in items: for box in item.boxes: box.collision_left.clear() box.collision_right.clear() left = [] for item in items: for candidate in reversed(left): for box in item.boxes: for candidate_box in candidate.boxes: if v_overlap(box, candidate_box): if box.is_in_collision_left_tree(candidate_box): continue box.collision_left.append(candidate_box) candidate_box.collision_right.append(box) left.append(item) def v_overlap(a, b): return max(a.get_top(), b.get_top()) < min(a.get_bottom(), b.get_bottom()) def sort_items_by_kind_order(items, order): order_index = {group: i for i, group in enumerate(order)} return sorted( sorted(items, key=lambda item: item.name), key=lambda item: order_index[item.kind], ) def unique_items(items): result = [] for item in items: if item not in result: result.append(item) return result def kind_order_index(items): result = {} for item in items: if item.kind not in result: result[item.kind] = len(result) return result def print_regions(regions): print("We have regions:") for region in regions: print(f"Region: {region['leftwall']} - {region['rightwall']}") for item in region["items"]: print(f" Item: {item}") print(f" Width: {item.get_width()}") print(f" Height: {item.get_bottom() - item.get_top()}") print(f" x-pos: {item.get_left()}") print(f" y-pos: {item.get_top()}") for box in item.boxes: print(f" Box: {box}") print(f" left-collision: {[c.item for c in box.collision_left]}") print(f" right-collision: {[c.item for c in box.collision_right]}")