Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions CodenameOne/src/com/codename1/components/ToastBar.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
@@ -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}.
*
* <p>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.</p>
*/
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);
}
}
Binary file added scripts/ios/screenshots/ToastBarTopPosition.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading