Skip to content

Commit f75be70

Browse files
committed
fix: unify dock split divider affordance across layouts
Use a CSS-only divider styling approach with shared DockSplitPane selectors, 2px divider padding, and consistent hover feedback in light/dark themes. Add DockLayoutEngine regression coverage to verify split-pane style class assignment and divider-node presence after DnD and rebuild flows. Promote Unreleased entries to v0.6.3 in CHANGELOG and refresh the MainDemo preview image for the updated splitter look.
1 parent a7e2e48 commit f75be70

5 files changed

Lines changed: 221 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ Changelog categories for `Unreleased` and new release sections (in order, includ
1616

1717
## Unreleased
1818

19+
## v0.6.3 - 2026-03-09
20+
21+
### UI and Interaction
22+
- Improved DockSplitPane divider usability: both orientations now use a consistent 4px divider thickness with explicit resize cursors, and divider hover highlighting now matches the sidebar-resize interaction style.
23+
24+
### Tests
25+
- Added `DockLayoutEngineTest` coverage to verify DockSplitPane style-class assignment and divider-node presence after DnD/rebuild flows.
26+
1927
### Documentation
2028
- ✅ Added [docs/ANIMATED_GIFS.md](docs/ANIMATED_GIFS.md) as a dedicated animated feature showcase page (Drag & Drop, Side Bars, Snapping + Themes) and linked it from [README.md](README.md).
2129

docs/images/main-demo.png

153 Bytes
Loading

snapfx-core/src/main/resources/snapfx-dark.css

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,17 @@
2929

3030
.dock-split-pane > .split-pane-divider,
3131
.dock-split-pane:horizontal > .split-pane-divider,
32-
.dock-split-pane:vertical > .split-pane-divider {
32+
.dock-split-pane:vertical > .split-pane-divider,
33+
.dock-split-pane .split-pane-divider {
3334
-fx-background-color: #25272c;
3435
}
3536

37+
.dock-split-pane > .split-pane-divider:hover,
38+
.dock-split-pane:horizontal > .split-pane-divider:hover,
39+
.dock-split-pane:vertical > .split-pane-divider:hover,
40+
.dock-split-pane .split-pane-divider:hover {
41+
-fx-background-color: rgba(93, 169, 255, 0.20);
42+
}
3643
.dock-tab-pane .tab-header-area {
3744
-fx-background-color: #25272c;
3845
}

snapfx-core/src/main/resources/snapfx.css

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -253,21 +253,43 @@
253253
-fx-padding: 0;
254254
}
255255

256-
.dock-split-pane > .split-pane-divider {
256+
.dock-split-pane > .split-pane-divider,
257+
.dock-split-pane .split-pane-divider {
257258
-fx-background-color: derive(-fx-background, -10%);
258-
-fx-padding: 0 1 0 1;
259+
-fx-padding: 0;
260+
-fx-background-insets: 0;
259261
}
260262

261-
.dock-split-pane:horizontal > .split-pane-divider {
262-
-fx-background-color: derive(-fx-background, -10%);
263-
-fx-padding: 0 1 0 1;
263+
.dock-split-pane:horizontal > .split-pane-divider,
264+
.dock-split-pane:horizontal .split-pane-divider {
265+
-fx-padding:2px;
266+
-fx-cursor: h_resize;
264267
}
265268

266-
.dock-split-pane:vertical > .split-pane-divider {
267-
-fx-background-color: derive(-fx-background, -10%);
268-
-fx-padding: 1 0 1 0;
269+
.dock-split-pane:vertical > .split-pane-divider,
270+
.dock-split-pane:vertical .split-pane-divider {
271+
-fx-cursor: v_resize;
272+
-fx-padding:2px;
273+
}
274+
275+
.dock-split-pane > .split-pane-divider > .grabber,
276+
.dock-split-pane > .split-pane-divider > .horizontal-grabber,
277+
.dock-split-pane > .split-pane-divider > .vertical-grabber,
278+
.dock-split-pane .split-pane-divider .grabber,
279+
.dock-split-pane .split-pane-divider .horizontal-grabber,
280+
.dock-split-pane .split-pane-divider .vertical-grabber {
281+
-fx-background-color: transparent;
282+
-fx-background-insets: 0;
283+
-fx-padding: 0;
284+
-fx-opacity: 0;
269285
}
270286

287+
.dock-split-pane > .split-pane-divider:hover,
288+
.dock-split-pane:horizontal > .split-pane-divider:hover,
289+
.dock-split-pane:vertical > .split-pane-divider:hover,
290+
.dock-split-pane .split-pane-divider:hover {
291+
-fx-background-color: rgba(75, 140, 245, 0.18);
292+
}
271293
/* ===== DockTabPane ===== */
272294
.dock-tab-pane {
273295
-fx-background-color: -fx-background;

snapfx-core/src/test/java/org/snapfx/view/DockLayoutEngineTest.java

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@
3737
import java.util.ArrayList;
3838
import java.util.List;
3939
import java.util.Map;
40+
import java.util.Objects;
41+
import java.util.stream.Stream;
42+
import java.util.concurrent.CountDownLatch;
43+
import java.util.concurrent.TimeUnit;
4044
import java.util.concurrent.atomic.AtomicReference;
4145

4246
import static org.junit.jupiter.api.Assertions.*;
@@ -438,6 +442,138 @@ void testBuildSplitPane() {
438442
SplitPane splitPane = (SplitPane) view;
439443
assertEquals(2, splitPane.getItems().size());
440444
}
445+
@Test
446+
void testHorizontalSplitPaneHasDockStyleAndDividerNode() {
447+
DockSplitPane root = new DockSplitPane(Orientation.HORIZONTAL);
448+
root.addChild(new DockNode(new Label("Left"), "Left"));
449+
root.addChild(new DockNode(new Label("Right"), "Right"));
450+
dockGraph.setRoot(root);
451+
452+
SplitPane splitPane = assertInstanceOf(SplitPane.class, layoutEngine.buildSceneGraph());
453+
Scene scene = createStyledScene(splitPane, 900, 600);
454+
scene.getRoot().applyCss();
455+
scene.getRoot().layout();
456+
waitForFxEvents();
457+
458+
assertTrue(splitPane.getStyleClass().contains(DockThemeStyleClasses.DOCK_SPLIT_PANE));
459+
assertTrue(countDirectDividerNodes(splitPane) >= 1, "Expected at least one direct divider node");
460+
}
461+
462+
@Test
463+
void testVerticalSplitPaneHasDockStyleAndDividerNode() {
464+
DockSplitPane root = new DockSplitPane(Orientation.VERTICAL);
465+
root.addChild(new DockNode(new Label("Top"), "Top"));
466+
root.addChild(new DockNode(new Label("Bottom"), "Bottom"));
467+
dockGraph.setRoot(root);
468+
469+
SplitPane splitPane = assertInstanceOf(SplitPane.class, layoutEngine.buildSceneGraph());
470+
Scene scene = createStyledScene(splitPane, 900, 600);
471+
scene.getRoot().applyCss();
472+
scene.getRoot().layout();
473+
waitForFxEvents();
474+
475+
assertTrue(splitPane.getStyleClass().contains(DockThemeStyleClasses.DOCK_SPLIT_PANE));
476+
assertTrue(countDirectDividerNodes(splitPane) >= 1, "Expected at least one direct divider node");
477+
}
478+
479+
@Test
480+
void testVerticalSplitPaneDividerNodesUpdateAfterAddingChild() {
481+
DockSplitPane root = new DockSplitPane(Orientation.VERTICAL);
482+
root.addChild(new DockNode(new Label("Top"), "Top"));
483+
root.addChild(new DockNode(new Label("Middle"), "Middle"));
484+
dockGraph.setRoot(root);
485+
486+
SplitPane splitPane = assertInstanceOf(SplitPane.class, layoutEngine.buildSceneGraph());
487+
Scene scene = createStyledScene(splitPane, 900, 700);
488+
scene.getRoot().applyCss();
489+
scene.getRoot().layout();
490+
waitForFxEvents();
491+
492+
assertTrue(splitPane.getStyleClass().contains(DockThemeStyleClasses.DOCK_SPLIT_PANE));
493+
assertTrue(countDirectDividerNodes(splitPane) >= 1, "Expected one divider node for two split items");
494+
495+
root.addChild(new DockNode(new Label("Bottom"), "Bottom"));
496+
scene.getRoot().applyCss();
497+
scene.getRoot().layout();
498+
waitForFxEvents();
499+
500+
assertTrue(splitPane.getDividers().size() >= 2, "Expected an additional divider after adding third child");
501+
assertTrue(countDirectDividerNodes(splitPane) >= 2, "Expected direct divider nodes to match multi-split layout");
502+
}
503+
504+
@Test
505+
void testDividerNodesRemainPresentAfterMovingPropertiesNodeToBottomAcrossFullWidth() {
506+
DockNode projectNode = new DockNode(new Label("Project"), "Project Explorer");
507+
DockNode editorNode = new DockNode(new Label("Editor"), "Main.java");
508+
DockNode propertiesNode = new DockNode(new Label("Properties"), "Properties");
509+
DockNode consoleNode = new DockNode(new Label("Console"), "Console");
510+
DockNode tasksNode = new DockNode(new Label("Tasks"), "Tasks");
511+
512+
dockGraph.setRoot(projectNode);
513+
dockGraph.dock(editorNode, projectNode, DockPosition.RIGHT);
514+
dockGraph.dock(propertiesNode, editorNode, DockPosition.RIGHT);
515+
dockGraph.dock(consoleNode, editorNode, DockPosition.BOTTOM);
516+
dockGraph.dock(tasksNode, consoleNode, DockPosition.CENTER);
517+
518+
DockElement initialRoot = dockGraph.getRoot();
519+
assertNotNull(initialRoot);
520+
dockGraph.move(propertiesNode, initialRoot, DockPosition.BOTTOM);
521+
522+
SplitPane rootSplit = assertInstanceOf(SplitPane.class, layoutEngine.buildSceneGraph());
523+
assertEquals(Orientation.VERTICAL, rootSplit.getOrientation());
524+
525+
Scene scene = createStyledScene(rootSplit, 1200, 800);
526+
scene.getRoot().applyCss();
527+
scene.getRoot().layout();
528+
waitForFxEvents();
529+
530+
assertTrue(rootSplit.getStyleClass().contains(DockThemeStyleClasses.DOCK_SPLIT_PANE));
531+
assertTrue(countDirectDividerNodes(rootSplit) >= 1, "Expected root split to expose divider node");
532+
533+
List<SplitPane> nestedVerticalSplits = rootSplit.lookupAll(".split-pane").stream()
534+
.filter(SplitPane.class::isInstance)
535+
.map(SplitPane.class::cast)
536+
.filter(split -> split != rootSplit && split.getOrientation() == Orientation.VERTICAL)
537+
.toList();
538+
nestedVerticalSplits.forEach(split -> {
539+
assertTrue(split.getStyleClass().contains(DockThemeStyleClasses.DOCK_SPLIT_PANE));
540+
if (split.getItems().size() > 1) {
541+
assertTrue(countDirectDividerNodes(split) >= 1, "Expected divider nodes for nested multi-item split");
542+
}
543+
});
544+
}
545+
546+
@Test
547+
void testAllDockSplitPaneViewsRetainStyleClassAfterDnDRebuild() {
548+
DockNode projectNode = new DockNode(new Label("Project"), "Project Explorer");
549+
DockNode editorNode = new DockNode(new Label("Editor"), "Main.java");
550+
DockNode propertiesNode = new DockNode(new Label("Properties"), "Properties");
551+
DockNode consoleNode = new DockNode(new Label("Console"), "Console");
552+
DockNode tasksNode = new DockNode(new Label("Tasks"), "Tasks");
553+
554+
dockGraph.setRoot(projectNode);
555+
dockGraph.dock(editorNode, projectNode, DockPosition.RIGHT);
556+
dockGraph.dock(propertiesNode, editorNode, DockPosition.RIGHT);
557+
dockGraph.dock(consoleNode, editorNode, DockPosition.BOTTOM);
558+
dockGraph.dock(tasksNode, consoleNode, DockPosition.CENTER);
559+
560+
DockElement rootBeforeMove = dockGraph.getRoot();
561+
assertNotNull(rootBeforeMove);
562+
dockGraph.move(propertiesNode, rootBeforeMove, DockPosition.BOTTOM);
563+
564+
SplitPane rootSplit = assertInstanceOf(SplitPane.class, layoutEngine.buildSceneGraph());
565+
Scene scene = createStyledScene(rootSplit, 1200, 800);
566+
scene.getRoot().applyCss();
567+
scene.getRoot().layout();
568+
waitForFxEvents();
569+
570+
List<SplitPane> splitPanes = collectDockSplitPanes(rootSplit);
571+
assertFalse(splitPanes.isEmpty(), "Expected at least one split pane in dock layout");
572+
splitPanes.forEach(splitPane -> assertTrue(
573+
splitPane.getStyleClass().contains(DockThemeStyleClasses.DOCK_SPLIT_PANE),
574+
"Expected split pane to contain style class '" + DockThemeStyleClasses.DOCK_SPLIT_PANE + "'"
575+
));
576+
}
441577

442578
@Test
443579
void testAddElementZonesHandlesSmallLeafBoundsWithoutClampException() {
@@ -836,7 +972,46 @@ void testLargeLayoutCacheCleanupDuringDetachAttachCycles() {
836972
}
837973
}
838974
}
975+
private List<Node> getDirectDividerNodes(SplitPane splitPane) {
976+
return splitPane.getChildrenUnmodifiable().stream()
977+
.filter(child -> child.getStyleClass().contains("split-pane-divider"))
978+
.toList();
979+
}
980+
981+
private int countDirectDividerNodes(SplitPane splitPane) {
982+
return getDirectDividerNodes(splitPane).size();
983+
}
839984

985+
private Scene createStyledScene(Node contentNode, double width, double height) {
986+
Scene scene = new Scene(new StackPane(contentNode), width, height);
987+
String stylesheetUrl = Objects.requireNonNull(
988+
DockLayoutEngineTest.class.getResource("/snapfx.css"),
989+
"Expected snapfx.css test resource to be available"
990+
).toExternalForm();
991+
scene.getStylesheets().add(stylesheetUrl);
992+
return scene;
993+
}
994+
995+
private List<SplitPane> collectDockSplitPanes(SplitPane rootSplit) {
996+
return Stream.concat(
997+
Stream.of(rootSplit),
998+
rootSplit.lookupAll(".split-pane").stream()
999+
.filter(SplitPane.class::isInstance)
1000+
.map(SplitPane.class::cast)
1001+
)
1002+
.distinct()
1003+
.toList();
1004+
}
1005+
private void waitForFxEvents() {
1006+
CountDownLatch latch = new CountDownLatch(1);
1007+
Platform.runLater(latch::countDown);
1008+
try {
1009+
assertTrue(latch.await(5, TimeUnit.SECONDS), "Timed out waiting for JavaFX events");
1010+
} catch (InterruptedException interruptedException) {
1011+
Thread.currentThread().interrupt();
1012+
fail("Interrupted while waiting for JavaFX events", interruptedException);
1013+
}
1014+
}
8401015
private List<DockNode> buildLargeLayout(int nodeCount) {
8411016
List<DockNode> nodes = new ArrayList<>(nodeCount);
8421017
DockPosition[] positions = {

0 commit comments

Comments
 (0)