diff --git a/CodenameOne/src/com/codename1/components/ToastBar.java b/CodenameOne/src/com/codename1/components/ToastBar.java index a86efe9e46..94647a90be 100644 --- a/CodenameOne/src/com/codename1/components/ToastBar.java +++ b/CodenameOne/src/com/codename1/components/ToastBar.java @@ -687,9 +687,15 @@ private ToastBarComponent getToastBarComponent(boolean create) { s.setPaddingUnit(Style.UNIT_TYPE_PIXELS); s.setPaddingBottom(safeBottomMargin); } else if (position == Component.TOP && safeArea.getY() > 0) { - Style s = c.getAllStyles(); - s.setPaddingUnit(Style.UNIT_TYPE_PIXELS); - s.setPaddingTop(safeArea.getY()); + Container parent = c.getParent(); + if (parent != null) { + int neededPadding = safeArea.getY() - parent.getAbsoluteY(); + if (neededPadding > 0) { + Style s = c.getAllStyles(); + s.setPaddingUnit(Style.UNIT_TYPE_PIXELS); + s.setPaddingTop(neededPadding); + } + } } return c; diff --git a/maven/core-unittests/src/test/java/com/codename1/components/ToastBarTest.java b/maven/core-unittests/src/test/java/com/codename1/components/ToastBarTest.java index 517935f2a4..cecc4c3f1a 100644 --- a/maven/core-unittests/src/test/java/com/codename1/components/ToastBarTest.java +++ b/maven/core-unittests/src/test/java/com/codename1/components/ToastBarTest.java @@ -2,11 +2,27 @@ import com.codename1.junit.FormTest; import com.codename1.junit.UITestBase; +import com.codename1.testing.TestCodenameOneImplementation; +import com.codename1.ui.Component; +import com.codename1.ui.Container; +import com.codename1.ui.Display; +import com.codename1.ui.Form; +import com.codename1.ui.geom.Rectangle; +import com.codename1.ui.plaf.Style; + +import java.lang.reflect.Method; import static org.junit.jupiter.api.Assertions.*; class ToastBarTest extends UITestBase { + /** + * Upper bound on the default UIID padding (in pixels). Safe-area compensation + * values are typically 30-100+ px, so anything below this threshold means the + * safe-area code path did not add extra padding. + */ + private static final int MAX_DEFAULT_STYLE_PADDING = 10; + @FormTest void testGetInstanceReturnsSingleton() { ToastBar tb1 = ToastBar.getInstance(); @@ -75,4 +91,186 @@ void testShowMessageWithIcon() { ToastBar.Status status = ToastBar.showMessage("Test", '\uE000', 1000); assertNotNull(status); } + + // ---- Regression tests for ToastBar TOP position safe area padding ---- + + /** + * Invokes the private getToastBarComponent(boolean) method via reflection so + * that the component and its padding are set up without triggering animations. + */ + private Container invokeGetToastBarComponent(ToastBar tb) throws Exception { + Method m = ToastBar.class.getDeclaredMethod("getToastBarComponent", boolean.class); + m.setAccessible(true); + return (Container) m.invoke(tb, true); + } + + /** + * Cleans up the ToastBarComponent from the current form and resets the + * implementation's safe area to the default. + */ + private void cleanupToastBar(Container toastBarComponent) { + if (toastBarComponent != null) { + toastBarComponent.remove(); + } + Form f = Display.getInstance().getCurrent(); + if (f != null) { + f.putClientProperty("ToastBarComponent", null); + } + implementation.setDisplaySafeArea(null); + } + + /** + * Regression test: when position is TOP and the device has a safe area inset + * (e.g. notch), the ToastBar should NOT double-count the inset if its parent + * container is already positioned below the safe area boundary. + */ + @FormTest + void testTopPositionNoPaddingWhenParentBelowSafeArea() throws Exception { + int safeTop = 100; + // Simulate a device with a 100px top safe area inset + implementation.setDisplaySafeArea(new Rectangle(0, safeTop, 1080, 1920 - safeTop)); + + ToastBar tb = ToastBar.getInstance(); + tb.setPosition(Component.TOP); + + Form f = Display.getInstance().getCurrent(); + f.revalidate(); + + Container c = invokeGetToastBarComponent(tb); + assertNotNull(c, "ToastBarComponent should be created"); + + Container parent = c.getParent(); + assertNotNull(parent, "ToastBarComponent should have a parent"); + + // If the parent's absolute Y is at or beyond the safe area top, + // no extra padding should be added (this was the double-counting bug). + if (parent.getAbsoluteY() >= safeTop) { + int paddingTop = c.getStyle().getPaddingTop(); + assertTrue(paddingTop < safeTop, + "Top padding should NOT be the full safe area inset (" + safeTop + + ") when parent is already at or below the safe area, got: " + paddingTop); + } else { + // Parent is above the safe area boundary, padding should be the difference + int expectedPadding = safeTop - parent.getAbsoluteY(); + int paddingTop = c.getStyle().getPaddingTop(); + assertEquals(expectedPadding, paddingTop, + "Top padding should equal safeArea.getY() - parent.getAbsoluteY()"); + } + + cleanupToastBar(c); + } + + /** + * When position is TOP and the device has NO safe area inset (safeArea.getY() == 0), + * no extra top padding should be applied by the safe area logic. + */ + @FormTest + void testTopPositionNoPaddingWithoutSafeAreaInset() throws Exception { + // Default safe area: full display (y=0) + implementation.setDisplaySafeArea(null); + + ToastBar tb = ToastBar.getInstance(); + tb.setPosition(Component.TOP); + + Form f = Display.getInstance().getCurrent(); + f.revalidate(); + + Container c = invokeGetToastBarComponent(tb); + assertNotNull(c, "ToastBarComponent should be created"); + + // The default UIID may have some small padding, but it should be well below + // any safe area inset value. + int paddingTop = c.getStyle().getPaddingTop(); + assertTrue(paddingTop < MAX_DEFAULT_STYLE_PADDING, + "Top padding should not contain safe area compensation when no inset, got: " + paddingTop); + + cleanupToastBar(c); + } + + /** + * When position is BOTTOM and the device has a safe area bottom inset, + * the bottom padding should reflect the bottom safe area margin. + */ + @FormTest + void testBottomPositionPaddingWithSafeAreaInset() throws Exception { + int safeTop = 50; + int safeHeight = 1820; // leaves 50px at bottom (1920 - 50 - 1820 = 50) + implementation.setDisplaySafeArea(new Rectangle(0, safeTop, 1080, safeHeight)); + + ToastBar tb = ToastBar.getInstance(); + tb.setPosition(Component.BOTTOM); + + Form f = Display.getInstance().getCurrent(); + f.revalidate(); + + Container c = invokeGetToastBarComponent(tb); + assertNotNull(c, "ToastBarComponent should be created"); + + int expectedBottomPadding = 1920 - safeTop - safeHeight; // 50 + Style s = c.getStyle(); + assertEquals(expectedBottomPadding, s.getPaddingBottom(), + "Bottom padding should equal the safe area bottom margin"); + + cleanupToastBar(c); + } + + /** + * When position is BOTTOM and the device has no safe area inset, + * no extra bottom padding should be added by the safe area logic. + */ + @FormTest + void testBottomPositionNoPaddingWithoutSafeAreaInset() throws Exception { + implementation.setDisplaySafeArea(null); + + ToastBar tb = ToastBar.getInstance(); + tb.setPosition(Component.BOTTOM); + + Form f = Display.getInstance().getCurrent(); + f.revalidate(); + + Container c = invokeGetToastBarComponent(tb); + assertNotNull(c, "ToastBarComponent should be created"); + + // With full-screen safe area (y=0, height=displayHeight), bottom margin = 0 + // so no extra bottom padding should be applied. + int paddingBottom = c.getStyle().getPaddingBottom(); + assertTrue(paddingBottom < MAX_DEFAULT_STYLE_PADDING, + "Bottom padding should not contain safe area compensation, got: " + paddingBottom); + + cleanupToastBar(c); + } + + /** + * Verifies that the top padding equals the full safe area Y when the ToastBar's + * parent starts at absolute Y = 0 (e.g. no toolbar, fullscreen layered pane). + */ + @FormTest + void testTopPositionFullPaddingWhenParentAtOrigin() throws Exception { + int safeTop = 80; + implementation.setDisplaySafeArea(new Rectangle(0, safeTop, 1080, 1920 - safeTop)); + + ToastBar tb = ToastBar.getInstance(); + // Use the form layered pane which overlays the full form from Y=0 + tb.useFormLayeredPane(true); + tb.setPosition(Component.TOP); + + Form f = Display.getInstance().getCurrent(); + f.revalidate(); + + Container c = invokeGetToastBarComponent(tb); + assertNotNull(c, "ToastBarComponent should be created"); + + Container parent = c.getParent(); + assertNotNull(parent, "ToastBarComponent should have a parent"); + + // FormLayeredPane starts at absolute Y=0, so full safe area padding is needed + if (parent.getAbsoluteY() == 0) { + int paddingTop = c.getStyle().getPaddingTop(); + assertEquals(safeTop, paddingTop, + "Top padding should equal safeArea.getY() when parent is at Y=0"); + } + + cleanupToastBar(c); + tb.useFormLayeredPane(false); + } } diff --git a/maven/core-unittests/src/test/java/com/codename1/testing/TestCodenameOneImplementation.java b/maven/core-unittests/src/test/java/com/codename1/testing/TestCodenameOneImplementation.java index b8eafc71f7..ce54977d3e 100644 --- a/maven/core-unittests/src/test/java/com/codename1/testing/TestCodenameOneImplementation.java +++ b/maven/core-unittests/src/test/java/com/codename1/testing/TestCodenameOneImplementation.java @@ -93,6 +93,7 @@ public class TestCodenameOneImplementation extends CodenameOneImplementation { private Dimension desktopSize = new Dimension(displayWidth, displayHeight); private Dimension lastWindowSize; private Rectangle windowBounds = new Rectangle(0, 0, displayWidth, displayHeight); + private Rectangle displaySafeArea = null; private int deviceDensity = Display.DENSITY_MEDIUM; private boolean portrait = true; private boolean tablet = false; @@ -1096,6 +1097,7 @@ public void reset() { displayHeight = 1920; desktopSize = new Dimension(displayWidth, displayHeight); windowBounds = new Rectangle(0, 0, displayWidth, displayHeight); + displaySafeArea = null; lastWindowSize = null; nativeTitle = false; softkeyCount = 2; @@ -1126,6 +1128,27 @@ public void setDisplaySize(int width, int height) { this.displayHeight = height; } + /** + * Sets a custom display safe area to simulate devices with notches or safe area insets. + * Pass {@code null} to revert to the default behavior (full display area). + */ + public void setDisplaySafeArea(Rectangle safeArea) { + this.displaySafeArea = safeArea; + } + + @Override + public Rectangle getDisplaySafeArea(Rectangle rect) { + if (displaySafeArea != null) { + if (rect == null) { + rect = new Rectangle(); + } + rect.setBounds(displaySafeArea.getX(), displaySafeArea.getY(), + displaySafeArea.getWidth(), displaySafeArea.getHeight()); + return rect; + } + return super.getDisplaySafeArea(rect); + } + public void setDeviceDensity(int density) { this.deviceDensity = density; } diff --git a/scripts/android/screenshots/ToastBarTopPosition.png b/scripts/android/screenshots/ToastBarTopPosition.png new file mode 100644 index 0000000000..31d21bf6ca Binary files /dev/null and b/scripts/android/screenshots/ToastBarTopPosition.png differ diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java index 6c9a6365be..90f7ddaa72 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java @@ -73,6 +73,7 @@ public final class Cn1ssDeviceRunner extends DeviceRunner { new TabsScreenshotTest(), new TextAreaAlignmentScreenshotTest(), new ValidatorLightweightPickerScreenshotTest(), + new ToastBarTopPositionScreenshotTest(), // Keep this as the last screenshot test; orientation changes can leak into subsequent screenshots. new OrientationLockScreenshotTest(), new InPlaceEditViewTest(), diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/ToastBarTopPositionScreenshotTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/ToastBarTopPositionScreenshotTest.java new file mode 100644 index 0000000000..3318b1470e --- /dev/null +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/ToastBarTopPositionScreenshotTest.java @@ -0,0 +1,51 @@ +package com.codenameone.examples.hellocodenameone.tests; + +import com.codename1.components.ToastBar; +import com.codename1.ui.Component; +import com.codename1.ui.Container; +import com.codename1.ui.FontImage; +import com.codename1.ui.Form; +import com.codename1.ui.Label; +import com.codename1.ui.layouts.BorderLayout; +import com.codename1.ui.layouts.BoxLayout; +import com.codename1.ui.util.UITimer; + +/** + * Screenshot test for ToastBar positioned at {@link Component#TOP}. + * + *
This verifies the fix for the issue where {@code ToastBar} with + * {@code setPosition(Component.TOP)} rendered spurious empty space above + * the message text because the safe-area inset was double-counted when + * the layered-pane parent was already below the safe-area boundary.
+ */ +public class ToastBarTopPositionScreenshotTest extends BaseTest { + private Form form; + private int originalPosition; + + @Override + public boolean runTest() { + originalPosition = ToastBar.getInstance().getPosition(); + + form = createForm("ToastBar Top", new BorderLayout(), "ToastBarTopPosition"); + + Container content = new Container(BoxLayout.y()); + content.add(new Label("ToastBar at TOP position")); + content.add(new Label("No empty space should appear above the toast")); + form.add(BorderLayout.CENTER, content); + + form.show(); + return true; + } + + @Override + protected void registerReadyCallback(Form parent, Runnable run) { + ToastBar tb = ToastBar.getInstance(); + tb.setPosition(Component.TOP); + + // Use a long timeout so the toast stays visible for the screenshot + ToastBar.showMessage("Info message at top", FontImage.MATERIAL_INFO, 30000); + + // Wait for the toast animation to complete before taking the screenshot + UITimer.timer(2000, false, parent, run); + } +} diff --git a/scripts/ios/screenshots/ToastBarTopPosition.png b/scripts/ios/screenshots/ToastBarTopPosition.png new file mode 100644 index 0000000000..b57d5808f2 Binary files /dev/null and b/scripts/ios/screenshots/ToastBarTopPosition.png differ