Skip to content

Commit 64b9c78

Browse files
Copilotshai-almog
andauthored
Fix ToastBar empty space above text when positioned at TOP (#4721)
* Fix ToastBar empty space above text when positioned at TOP The safe area top padding was unconditionally set to safeArea.getY(), but the layered pane (which wraps the content pane) is already positioned below the toolbar that accounts for the safe area inset. This caused double-counting of the inset, creating empty space above the ToastBar text. Now computes the needed padding as the difference between the safe area top inset and the parent container's absolute Y position, so padding is only added when the ToastBar actually extends into the unsafe area. Agent-Logs-Url: https://github.com/codenameone/CodenameOne/sessions/c374fa90-ccba-426b-b2d3-5e2f430bf1fe Co-authored-by: shai-almog <67850168+shai-almog@users.noreply.github.com> * Skip safe area top padding when parent is null to avoid incorrect calculation Agent-Logs-Url: https://github.com/codenameone/CodenameOne/sessions/c374fa90-ccba-426b-b2d3-5e2f430bf1fe Co-authored-by: shai-almog <67850168+shai-almog@users.noreply.github.com> * Add regression tests for ToastBar TOP position safe area padding Add five regression tests to ToastBarTest covering: - TOP position with safe area inset when parent is below safe area (the original bug: should not double-count the inset) - TOP position without safe area inset (no extra padding) - BOTTOM position with safe area bottom inset (correct padding) - BOTTOM position without safe area inset (no extra padding) - TOP position with parent at Y=0 using FormLayeredPane (full safe area padding needed) Also adds setDisplaySafeArea/getDisplaySafeArea to TestCodenameOneImplementation to simulate devices with notches. Agent-Logs-Url: https://github.com/codenameone/CodenameOne/sessions/d40eb36c-4528-4297-97d4-4f8144175502 Co-authored-by: shai-almog <67850168+shai-almog@users.noreply.github.com> * Address review feedback: extract cleanup helper and replace magic number with constant Agent-Logs-Url: https://github.com/codenameone/CodenameOne/sessions/d40eb36c-4528-4297-97d4-4f8144175502 Co-authored-by: shai-almog <67850168+shai-almog@users.noreply.github.com> * Add ToastBar TOP position screenshot test for visual regression Add ToastBarTopPositionScreenshotTest that shows a ToastBar message at the TOP position to visually verify no spurious empty space appears above the toast text. Register it in Cn1ssDeviceRunner. Agent-Logs-Url: https://github.com/codenameone/CodenameOne/sessions/eaa2d20f-fd1b-44cd-b3b8-fda8d8155d7a Co-authored-by: shai-almog <67850168+shai-almog@users.noreply.github.com> * Add files via upload Signed-off-by: Shai Almog <67850168+shai-almog@users.noreply.github.com> * Add files via upload Signed-off-by: Shai Almog <67850168+shai-almog@users.noreply.github.com> --------- Signed-off-by: Shai Almog <67850168+shai-almog@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: shai-almog <67850168+shai-almog@users.noreply.github.com>
1 parent 0aab53c commit 64b9c78

7 files changed

Lines changed: 282 additions & 3 deletions

File tree

CodenameOne/src/com/codename1/components/ToastBar.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -687,9 +687,15 @@ private ToastBarComponent getToastBarComponent(boolean create) {
687687
s.setPaddingUnit(Style.UNIT_TYPE_PIXELS);
688688
s.setPaddingBottom(safeBottomMargin);
689689
} else if (position == Component.TOP && safeArea.getY() > 0) {
690-
Style s = c.getAllStyles();
691-
s.setPaddingUnit(Style.UNIT_TYPE_PIXELS);
692-
s.setPaddingTop(safeArea.getY());
690+
Container parent = c.getParent();
691+
if (parent != null) {
692+
int neededPadding = safeArea.getY() - parent.getAbsoluteY();
693+
if (neededPadding > 0) {
694+
Style s = c.getAllStyles();
695+
s.setPaddingUnit(Style.UNIT_TYPE_PIXELS);
696+
s.setPaddingTop(neededPadding);
697+
}
698+
}
693699
}
694700

695701
return c;

maven/core-unittests/src/test/java/com/codename1/components/ToastBarTest.java

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,27 @@
22

33
import com.codename1.junit.FormTest;
44
import com.codename1.junit.UITestBase;
5+
import com.codename1.testing.TestCodenameOneImplementation;
6+
import com.codename1.ui.Component;
7+
import com.codename1.ui.Container;
8+
import com.codename1.ui.Display;
9+
import com.codename1.ui.Form;
10+
import com.codename1.ui.geom.Rectangle;
11+
import com.codename1.ui.plaf.Style;
12+
13+
import java.lang.reflect.Method;
514

615
import static org.junit.jupiter.api.Assertions.*;
716

817
class ToastBarTest extends UITestBase {
918

19+
/**
20+
* Upper bound on the default UIID padding (in pixels). Safe-area compensation
21+
* values are typically 30-100+ px, so anything below this threshold means the
22+
* safe-area code path did not add extra padding.
23+
*/
24+
private static final int MAX_DEFAULT_STYLE_PADDING = 10;
25+
1026
@FormTest
1127
void testGetInstanceReturnsSingleton() {
1228
ToastBar tb1 = ToastBar.getInstance();
@@ -75,4 +91,186 @@ void testShowMessageWithIcon() {
7591
ToastBar.Status status = ToastBar.showMessage("Test", '\uE000', 1000);
7692
assertNotNull(status);
7793
}
94+
95+
// ---- Regression tests for ToastBar TOP position safe area padding ----
96+
97+
/**
98+
* Invokes the private getToastBarComponent(boolean) method via reflection so
99+
* that the component and its padding are set up without triggering animations.
100+
*/
101+
private Container invokeGetToastBarComponent(ToastBar tb) throws Exception {
102+
Method m = ToastBar.class.getDeclaredMethod("getToastBarComponent", boolean.class);
103+
m.setAccessible(true);
104+
return (Container) m.invoke(tb, true);
105+
}
106+
107+
/**
108+
* Cleans up the ToastBarComponent from the current form and resets the
109+
* implementation's safe area to the default.
110+
*/
111+
private void cleanupToastBar(Container toastBarComponent) {
112+
if (toastBarComponent != null) {
113+
toastBarComponent.remove();
114+
}
115+
Form f = Display.getInstance().getCurrent();
116+
if (f != null) {
117+
f.putClientProperty("ToastBarComponent", null);
118+
}
119+
implementation.setDisplaySafeArea(null);
120+
}
121+
122+
/**
123+
* Regression test: when position is TOP and the device has a safe area inset
124+
* (e.g. notch), the ToastBar should NOT double-count the inset if its parent
125+
* container is already positioned below the safe area boundary.
126+
*/
127+
@FormTest
128+
void testTopPositionNoPaddingWhenParentBelowSafeArea() throws Exception {
129+
int safeTop = 100;
130+
// Simulate a device with a 100px top safe area inset
131+
implementation.setDisplaySafeArea(new Rectangle(0, safeTop, 1080, 1920 - safeTop));
132+
133+
ToastBar tb = ToastBar.getInstance();
134+
tb.setPosition(Component.TOP);
135+
136+
Form f = Display.getInstance().getCurrent();
137+
f.revalidate();
138+
139+
Container c = invokeGetToastBarComponent(tb);
140+
assertNotNull(c, "ToastBarComponent should be created");
141+
142+
Container parent = c.getParent();
143+
assertNotNull(parent, "ToastBarComponent should have a parent");
144+
145+
// If the parent's absolute Y is at or beyond the safe area top,
146+
// no extra padding should be added (this was the double-counting bug).
147+
if (parent.getAbsoluteY() >= safeTop) {
148+
int paddingTop = c.getStyle().getPaddingTop();
149+
assertTrue(paddingTop < safeTop,
150+
"Top padding should NOT be the full safe area inset (" + safeTop
151+
+ ") when parent is already at or below the safe area, got: " + paddingTop);
152+
} else {
153+
// Parent is above the safe area boundary, padding should be the difference
154+
int expectedPadding = safeTop - parent.getAbsoluteY();
155+
int paddingTop = c.getStyle().getPaddingTop();
156+
assertEquals(expectedPadding, paddingTop,
157+
"Top padding should equal safeArea.getY() - parent.getAbsoluteY()");
158+
}
159+
160+
cleanupToastBar(c);
161+
}
162+
163+
/**
164+
* When position is TOP and the device has NO safe area inset (safeArea.getY() == 0),
165+
* no extra top padding should be applied by the safe area logic.
166+
*/
167+
@FormTest
168+
void testTopPositionNoPaddingWithoutSafeAreaInset() throws Exception {
169+
// Default safe area: full display (y=0)
170+
implementation.setDisplaySafeArea(null);
171+
172+
ToastBar tb = ToastBar.getInstance();
173+
tb.setPosition(Component.TOP);
174+
175+
Form f = Display.getInstance().getCurrent();
176+
f.revalidate();
177+
178+
Container c = invokeGetToastBarComponent(tb);
179+
assertNotNull(c, "ToastBarComponent should be created");
180+
181+
// The default UIID may have some small padding, but it should be well below
182+
// any safe area inset value.
183+
int paddingTop = c.getStyle().getPaddingTop();
184+
assertTrue(paddingTop < MAX_DEFAULT_STYLE_PADDING,
185+
"Top padding should not contain safe area compensation when no inset, got: " + paddingTop);
186+
187+
cleanupToastBar(c);
188+
}
189+
190+
/**
191+
* When position is BOTTOM and the device has a safe area bottom inset,
192+
* the bottom padding should reflect the bottom safe area margin.
193+
*/
194+
@FormTest
195+
void testBottomPositionPaddingWithSafeAreaInset() throws Exception {
196+
int safeTop = 50;
197+
int safeHeight = 1820; // leaves 50px at bottom (1920 - 50 - 1820 = 50)
198+
implementation.setDisplaySafeArea(new Rectangle(0, safeTop, 1080, safeHeight));
199+
200+
ToastBar tb = ToastBar.getInstance();
201+
tb.setPosition(Component.BOTTOM);
202+
203+
Form f = Display.getInstance().getCurrent();
204+
f.revalidate();
205+
206+
Container c = invokeGetToastBarComponent(tb);
207+
assertNotNull(c, "ToastBarComponent should be created");
208+
209+
int expectedBottomPadding = 1920 - safeTop - safeHeight; // 50
210+
Style s = c.getStyle();
211+
assertEquals(expectedBottomPadding, s.getPaddingBottom(),
212+
"Bottom padding should equal the safe area bottom margin");
213+
214+
cleanupToastBar(c);
215+
}
216+
217+
/**
218+
* When position is BOTTOM and the device has no safe area inset,
219+
* no extra bottom padding should be added by the safe area logic.
220+
*/
221+
@FormTest
222+
void testBottomPositionNoPaddingWithoutSafeAreaInset() throws Exception {
223+
implementation.setDisplaySafeArea(null);
224+
225+
ToastBar tb = ToastBar.getInstance();
226+
tb.setPosition(Component.BOTTOM);
227+
228+
Form f = Display.getInstance().getCurrent();
229+
f.revalidate();
230+
231+
Container c = invokeGetToastBarComponent(tb);
232+
assertNotNull(c, "ToastBarComponent should be created");
233+
234+
// With full-screen safe area (y=0, height=displayHeight), bottom margin = 0
235+
// so no extra bottom padding should be applied.
236+
int paddingBottom = c.getStyle().getPaddingBottom();
237+
assertTrue(paddingBottom < MAX_DEFAULT_STYLE_PADDING,
238+
"Bottom padding should not contain safe area compensation, got: " + paddingBottom);
239+
240+
cleanupToastBar(c);
241+
}
242+
243+
/**
244+
* Verifies that the top padding equals the full safe area Y when the ToastBar's
245+
* parent starts at absolute Y = 0 (e.g. no toolbar, fullscreen layered pane).
246+
*/
247+
@FormTest
248+
void testTopPositionFullPaddingWhenParentAtOrigin() throws Exception {
249+
int safeTop = 80;
250+
implementation.setDisplaySafeArea(new Rectangle(0, safeTop, 1080, 1920 - safeTop));
251+
252+
ToastBar tb = ToastBar.getInstance();
253+
// Use the form layered pane which overlays the full form from Y=0
254+
tb.useFormLayeredPane(true);
255+
tb.setPosition(Component.TOP);
256+
257+
Form f = Display.getInstance().getCurrent();
258+
f.revalidate();
259+
260+
Container c = invokeGetToastBarComponent(tb);
261+
assertNotNull(c, "ToastBarComponent should be created");
262+
263+
Container parent = c.getParent();
264+
assertNotNull(parent, "ToastBarComponent should have a parent");
265+
266+
// FormLayeredPane starts at absolute Y=0, so full safe area padding is needed
267+
if (parent.getAbsoluteY() == 0) {
268+
int paddingTop = c.getStyle().getPaddingTop();
269+
assertEquals(safeTop, paddingTop,
270+
"Top padding should equal safeArea.getY() when parent is at Y=0");
271+
}
272+
273+
cleanupToastBar(c);
274+
tb.useFormLayeredPane(false);
275+
}
78276
}

maven/core-unittests/src/test/java/com/codename1/testing/TestCodenameOneImplementation.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ public class TestCodenameOneImplementation extends CodenameOneImplementation {
9393
private Dimension desktopSize = new Dimension(displayWidth, displayHeight);
9494
private Dimension lastWindowSize;
9595
private Rectangle windowBounds = new Rectangle(0, 0, displayWidth, displayHeight);
96+
private Rectangle displaySafeArea = null;
9697
private int deviceDensity = Display.DENSITY_MEDIUM;
9798
private boolean portrait = true;
9899
private boolean tablet = false;
@@ -1096,6 +1097,7 @@ public void reset() {
10961097
displayHeight = 1920;
10971098
desktopSize = new Dimension(displayWidth, displayHeight);
10981099
windowBounds = new Rectangle(0, 0, displayWidth, displayHeight);
1100+
displaySafeArea = null;
10991101
lastWindowSize = null;
11001102
nativeTitle = false;
11011103
softkeyCount = 2;
@@ -1126,6 +1128,27 @@ public void setDisplaySize(int width, int height) {
11261128
this.displayHeight = height;
11271129
}
11281130

1131+
/**
1132+
* Sets a custom display safe area to simulate devices with notches or safe area insets.
1133+
* Pass {@code null} to revert to the default behavior (full display area).
1134+
*/
1135+
public void setDisplaySafeArea(Rectangle safeArea) {
1136+
this.displaySafeArea = safeArea;
1137+
}
1138+
1139+
@Override
1140+
public Rectangle getDisplaySafeArea(Rectangle rect) {
1141+
if (displaySafeArea != null) {
1142+
if (rect == null) {
1143+
rect = new Rectangle();
1144+
}
1145+
rect.setBounds(displaySafeArea.getX(), displaySafeArea.getY(),
1146+
displaySafeArea.getWidth(), displaySafeArea.getHeight());
1147+
return rect;
1148+
}
1149+
return super.getDisplaySafeArea(rect);
1150+
}
1151+
11291152
public void setDeviceDensity(int density) {
11301153
this.deviceDensity = density;
11311154
}
14.5 KB
Loading

scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ public final class Cn1ssDeviceRunner extends DeviceRunner {
7373
new TabsScreenshotTest(),
7474
new TextAreaAlignmentScreenshotTest(),
7575
new ValidatorLightweightPickerScreenshotTest(),
76+
new ToastBarTopPositionScreenshotTest(),
7677
// Keep this as the last screenshot test; orientation changes can leak into subsequent screenshots.
7778
new OrientationLockScreenshotTest(),
7879
new InPlaceEditViewTest(),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.codenameone.examples.hellocodenameone.tests;
2+
3+
import com.codename1.components.ToastBar;
4+
import com.codename1.ui.Component;
5+
import com.codename1.ui.Container;
6+
import com.codename1.ui.FontImage;
7+
import com.codename1.ui.Form;
8+
import com.codename1.ui.Label;
9+
import com.codename1.ui.layouts.BorderLayout;
10+
import com.codename1.ui.layouts.BoxLayout;
11+
import com.codename1.ui.util.UITimer;
12+
13+
/**
14+
* Screenshot test for ToastBar positioned at {@link Component#TOP}.
15+
*
16+
* <p>This verifies the fix for the issue where {@code ToastBar} with
17+
* {@code setPosition(Component.TOP)} rendered spurious empty space above
18+
* the message text because the safe-area inset was double-counted when
19+
* the layered-pane parent was already below the safe-area boundary.</p>
20+
*/
21+
public class ToastBarTopPositionScreenshotTest extends BaseTest {
22+
private Form form;
23+
private int originalPosition;
24+
25+
@Override
26+
public boolean runTest() {
27+
originalPosition = ToastBar.getInstance().getPosition();
28+
29+
form = createForm("ToastBar Top", new BorderLayout(), "ToastBarTopPosition");
30+
31+
Container content = new Container(BoxLayout.y());
32+
content.add(new Label("ToastBar at TOP position"));
33+
content.add(new Label("No empty space should appear above the toast"));
34+
form.add(BorderLayout.CENTER, content);
35+
36+
form.show();
37+
return true;
38+
}
39+
40+
@Override
41+
protected void registerReadyCallback(Form parent, Runnable run) {
42+
ToastBar tb = ToastBar.getInstance();
43+
tb.setPosition(Component.TOP);
44+
45+
// Use a long timeout so the toast stays visible for the screenshot
46+
ToastBar.showMessage("Info message at top", FontImage.MATERIAL_INFO, 30000);
47+
48+
// Wait for the toast animation to complete before taking the screenshot
49+
UITimer.timer(2000, false, parent, run);
50+
}
51+
}
82.9 KB
Loading

0 commit comments

Comments
 (0)