Port packed layout solver and overhaul layout settings

This commit is contained in:
ajp_anton
2026-05-13 03:06:17 +00:00
parent ba3b92d4d0
commit 06cc2e21d9
29 changed files with 5241 additions and 1302 deletions
@@ -0,0 +1,296 @@
package se.ajpanton.statusbartweak.runtime.layoutsolver;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import se.ajpanton.statusbartweak.runtime.layoutsolver.PackedStatusBarLayoutSolver.Box;
import se.ajpanton.statusbartweak.runtime.layoutsolver.PackedStatusBarLayoutSolver.Fallback;
import se.ajpanton.statusbartweak.runtime.layoutsolver.PackedStatusBarLayoutSolver.LayoutItem;
import se.ajpanton.statusbartweak.runtime.layoutsolver.PackedStatusBarLayoutSolver.Position;
import se.ajpanton.statusbartweak.runtime.layoutsolver.PackedStatusBarLayoutSolver.Spec;
public final class PackedStatusBarLayoutSolverTest {
@Test
public void sharedStatusPoolDoesNotExpandAcrossCutout() {
ArrayList<LayoutItem> items = baseItems(30, 2);
PackedStatusBarLayoutSolver.solve(cutoutSpec(), items);
LayoutItem status0 = find(items, "Status0");
LayoutItem status1 = find(items, "Status1");
assertFalse(overlapsCutout(status0));
assertFalse(overlapsCutout(status1));
assertTrue(status0.visibleIcons > 0);
assertTrue(status1.visibleIcons > 0);
assertTrue(status1.dot);
}
@Test
public void sharedNotificationPoolDoesNotExpandAcrossCutout() {
ArrayList<LayoutItem> items = baseItems(3, 40);
PackedStatusBarLayoutSolver.solve(cutoutSpec(), items);
LayoutItem notifs0 = find(items, "Notifs0");
LayoutItem notifs1 = find(items, "Notifs1");
LayoutItem notifs2 = find(items, "Notifs2");
assertFalse(overlapsCutout(notifs0));
assertFalse(overlapsCutout(notifs1));
assertFalse(overlapsCutout(notifs2));
}
@Test
public void zeroTruncationDoesNotExpandMultiBoxClock() {
LayoutItem clock = new LayoutItem(
"Clock",
"Clock",
Position.LEFT,
Fallback.LEFT,
LayoutItem.boxes(
new Box(102, 14, 33, 38),
new Box(0, 32, 134, 47)));
double beforeWidth = clock.width();
double actual = clock.truncateAtLeast(0);
assertTrue(actual == 0);
assertTrue(clock.width() == beforeWidth);
assertTrue(clock.boxes.get(0).width == 33);
assertTrue(clock.boxes.get(1).width == 134);
}
@Test
public void discreteTruncationUsesRealIconWidthsAndWallCompactDot() {
ArrayList<LayoutItem> items = new ArrayList<>();
items.add(new LayoutItem(
"Clock",
"Clock",
Position.LEFT,
Fallback.LEFT,
LayoutItem.boxes(
new Box(102, 14, 33, 38),
new Box(0, 32, 134, 47))));
LayoutItem status = iconItem("Status", "Status", "status", 5, 35, 197, 18, 35);
status.iconWidths = new ArrayList<>(Arrays.asList(35, 35, 35, 35, 57));
status.widthLimit = 197.0;
LayoutItem notifications = iconItem("Notifs", "Notifs", "notifications", 5, 39, 195, 36, 39);
notifications.compactDotWidth = 27;
items.add(status);
items.add(notifications);
PackedStatusBarLayoutSolver.solve(productionSpec(834, 387, 59, 11), items);
assertTrue(status.visibleIcons == 5);
assertFalse(status.dot);
assertTrue(notifications.visibleIcons == 0);
assertTrue(notifications.dot);
assertTrue(notifications.right() <= 387);
assertTrue(notifications.left() - status.right() >= 11);
}
@Test
public void middleLeftItemStacksAgainstCutoutWallInSplitRegion() {
ArrayList<LayoutItem> items = new ArrayList<>();
items.add(new LayoutItem(
"Clock",
"Clock",
Position.LEFT,
Fallback.LEFT,
LayoutItem.boxes(
new Box(102, 14, 33, 38),
new Box(0, 32, 134, 47))));
LayoutItem status = iconItem("Status", "Status", "status", 5, 35, 197, 18, 35);
status.iconWidths = new ArrayList<>(Arrays.asList(35, 35, 40, 34, 53));
status.widthLimit = 197.0;
LayoutItem notifications = new LayoutItem(
"Notifs",
"Notifs",
Position.MIDDLE,
Fallback.LEFT,
LayoutItem.boxes(new Box(0, 36, 156, 39)));
notifications.iconGroup = "notifications";
notifications.iconCount = 4;
notifications.visibleIcons = 4;
notifications.poolRemaining = 4;
notifications.iconWidth = 39;
notifications.iconWidths = new ArrayList<>(Arrays.asList(39, 39, 39, 39));
notifications.dotWidth = 39;
notifications.compactDotWidth = 27;
notifications.allowDot = true;
notifications.allowZero = false;
notifications.widthLimit = 156.0;
items.add(status);
items.add(notifications);
PackedStatusBarLayoutSolver.solve(productionSpec(834, 387, 59, 11), items);
assertTrue(notifications.right() <= 387);
assertTrue(notifications.left() >= status.right() + 11);
}
@Test
public void truncatedDotUsesAvailableIconSlotAndWallCroppedDot() {
LayoutItem notifications = iconItem(
"Notifs",
"Notifs",
"notifications",
2,
39,
78,
36,
39);
notifications.direction = PackedStatusBarLayoutSolver.Direction.RIGHT;
notifications.widthLimit = 70.0;
notifications.recomputeIconState();
assertTrue(notifications.visibleIcons == 1);
assertTrue(notifications.dot);
assertTrue(notifications.width() == 65.0);
}
private static Spec cutoutSpec() {
return new Spec(
1200,
850,
100,
new ArrayList<>(Arrays.asList("Notifs", "Status", "Chip")));
}
private static Spec productionSpec(int screenWidth, int cutoutLeft, int cutoutWidth, int padding) {
return new Spec(
screenWidth,
cutoutLeft,
cutoutWidth,
padding,
new ArrayList<>(Arrays.asList("Notifs", "Status", "Chip", "Clock")));
}
private static LayoutItem iconItem(
String name,
String kind,
String group,
int count,
int iconWidth,
int naturalWidth,
int top,
int height
) {
LayoutItem item = new LayoutItem(
name,
kind,
Position.LEFT,
Fallback.LEFT,
LayoutItem.boxes(new Box(0, top, naturalWidth, height)));
item.iconGroup = group;
item.iconCount = count;
item.visibleIcons = count;
item.poolRemaining = count;
item.iconWidth = iconWidth;
item.dotWidth = iconWidth;
item.compactDotWidth = iconWidth;
item.allowDot = true;
item.allowZero = false;
item.widthLimit = count * (double) iconWidth;
return item;
}
private static ArrayList<LayoutItem> baseItems(int statusIcons, int notificationIcons) {
ArrayList<LayoutItem> items = new ArrayList<>();
LayoutItem clock = new LayoutItem(
"Clock",
"Clock",
Position.LEFT,
Fallback.LEFT,
LayoutItem.boxes(
new Box(0, 0, 240, 40),
new Box(80, -20, 30, 30)));
clock.y = 100;
items.add(clock);
items.add(LayoutItem.fixed(
"Chip",
"Chip",
Position.MIDDLE,
Fallback.RIGHT,
400,
40,
90));
addIcon(items, "Notifs0", "Notifs", Position.LEFT, Fallback.LEFT, "notifications", notificationIcons, 0);
addIcon(items, "Notifs1", "Notifs", Position.LEFT, Fallback.LEFT, "notifications", notificationIcons, 50);
addIcon(items, "Notifs2", "Notifs", Position.LEFT, Fallback.LEFT, "notifications", notificationIcons, 30);
addIcon(items, "Status0", "Status", Position.RIGHT, Fallback.RIGHT, "status", statusIcons, 50);
addIcon(items, "Status1", "Status", Position.RIGHT, Fallback.RIGHT, "status", statusIcons, 100);
items.add(LayoutItem.fixed("Dummy0", "Dummy", Position.MIDDLE, Fallback.LEFT, 100, 30, 50));
items.add(LayoutItem.fixed("Dummy1", "Dummy", Position.MIDDLE, Fallback.LEFT, 100, 30, 0));
items.add(LayoutItem.fixed("Dummy2", "Dummy", Position.MIDDLE, Fallback.LEFT, 100, 30, 20));
configureIconGroups(items);
return sortByKindOrder(items, Arrays.asList("Clock", "Chip", "Status", "Notifs", "Dummy"));
}
private static void addIcon(
ArrayList<LayoutItem> items,
String name,
String kind,
Position position,
Fallback fallback,
String group,
int count,
int y
) {
items.add(LayoutItem.iconStrip(name, kind, position, fallback, group, count, 50, y));
}
private static void configureIconGroups(ArrayList<LayoutItem> items) {
ArrayList<LayoutItem> status = new ArrayList<>();
ArrayList<LayoutItem> notifications = new ArrayList<>();
for (LayoutItem item : items) {
if ("status".equals(item.iconGroup)) {
status.add(item);
} else if ("notifications".equals(item.iconGroup)) {
notifications.add(item);
}
}
configureGroup(status);
configureGroup(notifications);
}
private static void configureGroup(ArrayList<LayoutItem> items) {
for (int i = 0; i < items.size(); i++) {
LayoutItem item = items.get(i);
item.allowZero = i < items.size() - 1;
item.allowDot = i == items.size() - 1;
}
}
private static ArrayList<LayoutItem> sortByKindOrder(ArrayList<LayoutItem> items, List<String> order) {
ArrayList<LayoutItem> sorted = new ArrayList<>(items);
sorted.sort((a, b) -> {
int kind = Integer.compare(order.indexOf(a.kind), order.indexOf(b.kind));
return kind != 0 ? kind : a.name.compareTo(b.name);
});
return sorted;
}
private static LayoutItem find(ArrayList<LayoutItem> items, String name) {
for (LayoutItem item : items) {
if (name.equals(item.name)) {
return item;
}
}
throw new AssertionError("Missing item " + name);
}
private static boolean overlapsCutout(LayoutItem item) {
return Math.max(item.left(), 850) < Math.min(item.right(), 950);
}
}
@@ -0,0 +1,85 @@
package se.ajpanton.statusbartweak.runtime.render;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import java.util.ArrayList;
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.AnchorSide;
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds;
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.LayoutItemKind;
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.LayoutPosition;
public final class UnlockedLayoutBandTest {
@Test
public void compactNotificationDotUsesSolverWidthAfterIconShrinking() {
UnlockedLayoutBand band = UnlockedLayoutBand.icons(
LayoutItemKind.NOTIFICATIONS,
null,
null,
placements(6, 39, 39),
LayoutPosition.LEFT,
false,
AnchorSide.LEFT,
true);
band.applyPackedIconState(0, 0, true, 14);
band.place(388);
assertEquals(1, band.placements.size());
SnapshotPlacement dot = band.placements.get(0);
assertTrue(dot.overflowDot);
assertEquals(14, dot.bounds.width);
assertEquals(388, dot.bounds.left);
assertEquals(14, band.placedBounds.width);
}
@Test
public void packedIconStateUsesRequestedShrinkableIconSlice() {
ArrayList<SnapshotPlacement> placements = placements(6, 39, 39);
placements.add(1, new SnapshotPlacement(
null,
new Bounds(0, 36, 39, 39),
"existing-dot",
false,
true,
null,
new Bounds(0, 0, 39, 39),
0));
UnlockedLayoutBand band = UnlockedLayoutBand.icons(
LayoutItemKind.NOTIFICATIONS,
null,
null,
placements,
LayoutPosition.LEFT,
false,
AnchorSide.LEFT,
true);
band.applyPackedIconState(2, 3, false, 117);
assertEquals(3, band.placements.size());
assertEquals("icon2", band.placements.get(0).key);
assertEquals("icon3", band.placements.get(1).key);
assertEquals("icon4", band.placements.get(2).key);
}
private static ArrayList<SnapshotPlacement> placements(int count, int width, int height) {
ArrayList<SnapshotPlacement> result = new ArrayList<>();
for (int i = 0; i < count; i++) {
Bounds bounds = new Bounds(0, 36, width, height);
result.add(new SnapshotPlacement(
null,
bounds,
"icon" + i,
false,
false,
null,
new Bounds(0, 0, width, height),
0));
}
return result;
}
}