Skip to content

Commit 4af91e3

Browse files
committed
fix: guard drop-zone clamp bounds for tiny drag targets
Normalize drop-zone clamp ranges before calling Math.clamp so tiny leaf bounds cannot trigger IllegalArgumentException during drag hover. Add a DockLayoutEngine regression test for tiny bounds to ensure zone generation stays stable and does not throw in addElementZones. Update STATUS/DONE/ROADMAP/CHANGELOG to reflect the fix and 189 passing tests.
1 parent 2d6cc0f commit 4af91e3

6 files changed

Lines changed: 59 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ The changelog is grouped by release tags (`vX.Y.Z`) and includes an `Unreleased`
1010
- ✅ Removed obsolete JavaFX test JVM module flags that caused classpath/module warning noise (`24ac0bc`).
1111
- ✅ Stabilized JavaFX module-path test runtime without FXML assumptions (`e8197a9`).
1212
- ✅ Migrated plugin/dependency versions to `gradle/libs.versions.toml` and reduced duplicated build configuration (`517b5db`).
13-
- ✅ Expanded shortcut, context-menu, and demo-accelerator coverage in unit tests; full suite now runs with 188 tests.
13+
- ✅ Expanded shortcut, context-menu, and demo-accelerator coverage in unit tests; full suite now runs with 189 tests.
1414

1515
### Framework and UI
1616
- ✅ Added configurable framework keyboard shortcut API in `SnapFX` (`setShortcut`, `clearShortcut`, `resetShortcutsToDefaults`, `getShortcuts`).
@@ -23,6 +23,7 @@ The changelog is grouped by release tags (`vX.Y.Z`) and includes an `Unreleased`
2323
- ✅ Added close/float control-glyph icons to dock-node and tab context-menu actions for visual parity with header/tab buttons.
2424
- ✅ Reworked DockNode icon model to image-based rendering so each view creates its own icon node, preventing parent-sharing losses in floating title bars.
2525
- ✅ Floating title-bar icon now tracks active tab selection in floating tab layouts.
26+
- ✅ Hardened drop-zone edge-size clamping for tiny bounds so drag hover no longer throws `Math.clamp` min/max-order exceptions.
2627

2728
### Documentation
2829
- ✅ Split repository workflow content out of `README.md` into dedicated `CONTRIBUTING.md` and `RELEASING.md` (`6f93d0a`).

DONE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ SnapFX has been fully implemented with core functionality and is production-read
5959
- ✅ Full JPMS support with proper exports and opens
6060
- ✅ Compatible with Java 21 module system
6161

62-
### Testing (12 test classes, 188 tests)
62+
### Testing (12 test classes, 189 tests)
6363
-`DockGraphTest` (56 tests) - Tree manipulation and algorithms
6464
- **+11 regression tests** for critical bug fixes
6565
- Tests for empty container prevention
@@ -70,7 +70,7 @@ SnapFX has been fully implemented with core functionality and is production-read
7070
- Edge case tests for null/no-op/detached-target handling
7171
-`DockLayoutSerializerTest` (9 tests) - Persistence functionality
7272
- **+1 regression test** for locked state synchronization (2026-02-10)
73-
-`DockLayoutEngineTest` (28 tests) - View creation with TestFX, context-menu interaction coverage, and float-availability policy checks
73+
-`DockLayoutEngineTest` (29 tests) - View creation with TestFX, context-menu interaction coverage, float-availability policy checks, and tiny-bounds drop-zone clamp regression coverage
7474
- Memory cleanup tests for cache boundedness and undock/rebuild cycles
7575
- Layout optimization tests for empty/single-child roots
7676
-`SnapFXTest` (44 tests) - Hide/Restore + Floating Window API behavior plus configurable shortcut behavior

ROADMAP.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ This roadmap lists planned work only; completed/fixed history is tracked in `CHA
3737
### 1.1 Drag & Drop Improvements
3838
**Priority**: 🔴 Critical
3939

40+
-**Tiny-bounds drop-zone stability**: Drop-zone edge sizing now avoids `Math.clamp` min/max-order exceptions during drag hover on very small bounds
4041
- 📋 No planned items; see `CHANGELOG.md` for completed fixes.
4142

4243
---

STATUS.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
## Build Status
66

77
**Build**: `BUILD SUCCESSFUL`
8-
**Tests**: All 188 tests passing (latest full suite)
8+
**Tests**: All 189 tests passing (latest full suite)
99
**Module System**: Fully implemented (JPMS)
1010
**Demo App**: Running successfully
1111
**CI Automation**: GitHub Actions workflows added for push/PR tests and tag-triggered releases
@@ -57,6 +57,7 @@
5757
- ✅ Unresolved drops always trigger floating fallback (not only outside main scene)
5858
- ✅ Main layout drops accept drags originating from floating windows
5959
- ✅ Escape cancels active drag reliably, including while the mouse button remains pressed
60+
- ✅ Drop-zone sizing now guards tiny bounds so drag hover never throws `Math.clamp` min/max-order exceptions
6061

6162
### Persistence (100% ✅)
6263
- ✅ JSON serialization (Gson)
@@ -127,7 +128,7 @@
127128
### Testing (100% ✅)
128129
- ✅ DockGraphTest (56 tests, +11 regression tests)
129130
- ✅ DockLayoutSerializerTest (9 tests, +1 regression test)
130-
- ✅ DockLayoutEngineTest (28 tests) - Includes tab/header/splitter context-menu coverage and float-availability policy checks
131+
- ✅ DockLayoutEngineTest (29 tests) - Includes tab/header/splitter context-menu coverage, float-availability policy checks, and tiny-bounds drop-zone clamp regression coverage
131132
-**SnapFXTest (44 tests)** - Hide/Restore + Floating Window API tests plus configurable shortcut behavior
132133
- ✅ DockGraphSplitTargetDockingTest (1 test)
133134
- ✅ DockDragServiceTest (8 tests) - D&D visibility, tab-hover activation, float-detach callback behavior, and ESC drag-cancel handling
@@ -137,7 +138,7 @@
137138
- ✅ SimpleExampleTest (2 tests) - Stylesheet resource resolution behavior
138139
- ✅ MarkdownDocumentationConsistencyTest (12 tests) - Markdown consistency guardrails
139140
- ✅ AboutDialogTest (2 tests) - About dialog branding resources and credit link targets
140-
-**188/188 tests passing**
141+
-**189/189 tests passing**
141142
-**Performance tests for large layouts** (50+ nodes with stress move/cleanup operations)
142143
-**Memory leak cleanup tests** (cache boundedness, undock cleanup, large-layout detach/attach cycles)
143144
-**Edge case tests** (null inputs, detached nodes, invalid move targets, no-op revision checks)

src/main/java/com/github/beowolve/snapfx/view/DockLayoutEngine.java

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -791,10 +791,8 @@ private void addElementZones(DockElement element, Node view, Bounds bounds, int
791791
double zoneMin = isLeaf ? LEAF_DROP_ZONE_MIN_PX : DROP_ZONE_MIN_PX;
792792
double zoneMaxRatio = isLeaf ? LEAF_DROP_ZONE_MAX_RATIO : DROP_ZONE_MAX_RATIO;
793793

794-
double edgeW = Math.clamp(zoneBounds.getWidth() * zoneRatio,
795-
zoneMin, zoneBounds.getWidth() * zoneMaxRatio);
796-
double edgeH = Math.clamp(zoneBounds.getHeight() * zoneRatio,
797-
zoneMin, zoneBounds.getHeight() * zoneMaxRatio);
794+
double edgeW = calculateDropZoneEdgeSize(zoneBounds.getWidth(), zoneRatio, zoneMin, zoneMaxRatio);
795+
double edgeH = calculateDropZoneEdgeSize(zoneBounds.getHeight(), zoneRatio, zoneMin, zoneMaxRatio);
798796

799797
Bounds left = new BoundingBox(zoneBounds.getMinX(), zoneBounds.getMinY(), edgeW, zoneBounds.getHeight());
800798
Bounds right = new BoundingBox(zoneBounds.getMaxX() - edgeW, zoneBounds.getMinY(), edgeW, zoneBounds.getHeight());
@@ -814,6 +812,18 @@ private void addElementZones(DockElement element, Node view, Bounds bounds, int
814812
}
815813
}
816814

815+
private double calculateDropZoneEdgeSize(double span, double ratio, double minSize, double maxRatio) {
816+
if (span <= 0) {
817+
return 0;
818+
}
819+
double maxSize = span * maxRatio;
820+
if (maxSize <= 0) {
821+
return 0;
822+
}
823+
double normalizedMin = Math.min(minSize, maxSize);
824+
return Math.clamp(span * ratio, normalizedMin, maxSize);
825+
}
826+
817827
private Bounds buildCenterBounds(Bounds bounds, double edgeW, double edgeH) {
818828
double innerW = bounds.getWidth() - (edgeW * 2);
819829
double innerH = bounds.getHeight() - (edgeH * 2);

src/test/java/com/github/beowolve/snapfx/view/DockLayoutEngineTest.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import com.github.beowolve.snapfx.model.*;
66
import javafx.application.Platform;
77
import javafx.event.Event;
8+
import javafx.geometry.BoundingBox;
9+
import javafx.geometry.Bounds;
810
import javafx.geometry.Orientation;
911
import javafx.scene.Node;
1012
import javafx.scene.control.Button;
@@ -26,6 +28,7 @@
2628
import org.testfx.framework.junit5.ApplicationTest;
2729

2830
import java.lang.reflect.Field;
31+
import java.lang.reflect.Method;
2932
import java.util.ArrayList;
3033
import java.util.List;
3134
import java.util.Map;
@@ -347,6 +350,16 @@ void testBuildSplitPane() {
347350
assertEquals(2, splitPane.getItems().size());
348351
}
349352

353+
@Test
354+
void testAddElementZonesHandlesSmallLeafBoundsWithoutClampException() {
355+
DockNode node = new DockNode(new Label("Test"), "Node");
356+
List<DockDropZone> zones = new ArrayList<>();
357+
Bounds tinyBounds = new BoundingBox(0, 0, 43.0, 43.0);
358+
359+
assertDoesNotThrow(() -> invokeAddElementZones(node, new StackPane(), tinyBounds, 0, zones));
360+
assertEquals(5, zones.size());
361+
}
362+
350363
@Test
351364
void testSplitPaneContextMenuResetItemAppliesBalancedRatios() {
352365
DockNode node1 = new DockNode(new Label("Test1"), "Node 1");
@@ -681,4 +694,27 @@ private MouseEvent createPrimaryPressEvent(Node source, Node target) {
681694
new PickResult(target, 0, 0)
682695
);
683696
}
697+
698+
private void invokeAddElementZones(
699+
DockElement element,
700+
Node view,
701+
Bounds bounds,
702+
int depth,
703+
List<DockDropZone> zones
704+
) {
705+
try {
706+
Method method = DockLayoutEngine.class.getDeclaredMethod(
707+
"addElementZones",
708+
DockElement.class,
709+
Node.class,
710+
Bounds.class,
711+
int.class,
712+
List.class
713+
);
714+
method.setAccessible(true);
715+
method.invoke(layoutEngine, element, view, bounds, depth, zones);
716+
} catch (ReflectiveOperationException e) {
717+
throw new AssertionError("Unable to invoke DockLayoutEngine.addElementZones", e);
718+
}
719+
}
684720
}

0 commit comments

Comments
 (0)