diff --git a/README.md b/README.md
index 7acfd94..282419e 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,13 @@
-# SvgNode
+# SvgNode project
A lightweight, optimized JavaFX node for rendering SVG paths at any size. Fully supports FXML, property binding, and CSS styling.
-
+
-Usage with SVG Libraries
+SVG Libraries
-## SVG Libraries
+## Sampler with SVG Libraries
### Material Design

@@ -20,14 +20,30 @@ A lightweight, optimized JavaFX node for rendering SVG paths at any size. Fully
+## Table of Contents
+
+- [Features](#features)
+- [Installation](#installation)
+ - [Maven](#maven)
+ - [Gradle](#gradle)
+- [Usage](#usage)
+ - [Java](#java)
+ - [FXML](#fxml)
+ - [Use with SVG Libraries](#use-with-svg-libraries)
+- [Sampler](#sampler)
+- [API / Motivation](#api-and-motivation)
+
+---
+
## Features
- 🎨 Render any SVG path as a JavaFX node
-- 🔗 No dependencies – will use your provided JavaFX runtime
-- ⚡ Optimized to be efficient and have a tiny footprint due to the SvgNode extending from `Parent`, skipping size calculations and only initializing properties when needed
+- 🔗 No dependencies - will use your provided JavaFX runtime
+- ⚡ Optimized to be efficient and have a tiny footprint, so you can render 100 `SvgNode` without any problem.
+ - This is achieved by `SvgNode` extending from `Parent`, skipping size calculations and only initializing properties when needed
- 📐 Uniform rasterization with a single `size` property
- 📄 FXML-compatible with attribute and constant-based usage
-- 🎭 CSS-stylable via `.svg-node` and `.svg` style classes. By default, the SVG automatically adjusts its color based on the background – just like text!
+- 🎭 CSS-stylable via `.svg-node` and `.svg` style classes. By default, the SVG automatically adjusts its color based on the background - just like text
### With SVG Libraries
@@ -36,29 +52,29 @@ Will work fine with SVG Libraries (see usage [below](#use-with-svg-libraries)) s
- [SVG-Boostrap](https://github.com/Maran23/svg-bootstrap)
- [SVG-FontAwesome](https://github.com/Maran23/svg-fontawesome)
-## Requirements
+## Installation
+
+### Requirements
| Dependency | Version |
|------------|----------|
| Java | 25+ |
| JavaFX | 25+ |
-## Installation
-
### Maven
```xml
tools.maran
svgnode
- 1.0.0
+ 2.0.0
```
### Gradle
```groovy
-implementation 'tools.maran:svgnode:1.0.0'
+implementation 'tools.maran:svgnode:2.0.0'
```
## Usage
@@ -73,7 +89,7 @@ SvgNode defaultIcon = new SvgNode("M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z");
// Icon with a size and color
SvgNode icon32 = new SvgNode("M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z", 32);
-icon32.setSvgColor(Color.RED);
+icon32.setColor(Color.RED);
// Binding
SvgNode dynamic = new SvgNode();
@@ -86,7 +102,7 @@ dynamic.sizeProperty().bind(slider.valueProperty());
```xml
-
+
```
#### Use String or Enum constants from an Icon Class:
@@ -95,7 +111,7 @@ dynamic.sizeProperty().bind(slider.valueProperty());
-
+
@@ -104,7 +120,7 @@ dynamic.sizeProperty().bind(slider.valueProperty());
### Use with SVG Libraries
-`SvgNode` works with any library that provides SVGs as a path.
+`SvgNode` works with any library that provides SVGs as a path. Below are some examples where the SVGs are located in an Enum.
```java
import tools.maran.svgnode.SvgNode;
@@ -144,10 +160,12 @@ SvgNode iconFA = new SvgNode(FASolid.HOME.path());
The sampler is in the tests and can be used to show examples and manually test the `SvgNode`.
-Launch the `tools.maran.svgnode.manual.Sampler` class.
+Launch the following class located in the tests:
+```shell
+tools.maran.svgnode.manual.Sampler
+```
The sampler respects the light and dark color scheme of your OS, so you can see how it looks in both.
-
There are two categories in the sampler.
### SvgNode sampler
@@ -161,4 +179,49 @@ Shows the functionality of `SvgNode`.
Shows all SVGs in a grid from supported SVG libraries, ready to be explored.
-Contains all SVG libraries mentioned [above](#svg-libraries)
+Contains all SVG libraries mentioned [above](#svg-libraries).
+
+## API and Motivation
+
+### Motivation
+
+For a long time, the most common way to show icons in JavaFX was done by using Icon fonts. This works, but is not perfect, e.g. sometimes Icons are blurry or the size is not what I would expect. It is also tricky to get theming right, especially when supporting the light and dark color scheme.
+
+`SvgNode` aims to take that approach to a new level: Showing SVGs instead.
+
+While there is the `SvgPath` in JavaFX that does the heavy lifting of parsing the SVG path, rendering it pixel perfect is actually not that straight forward.
+In fact, even in JavaFX the SVGs (e.g. arrow) that are used rely on some weird CSS and sizing to get that done right. For example, the arrow in the TitledPane required a Region in a Region and a hardcoded padding.
+
+I think we can do better, and `SvgNode` encapsulates all that logic for you. It is always pixel perfect, works with most SVGs libraries out there and can easily change the color.
+
+### API
+
+Details about the API naming and changelog.
+
+#### Naming
+
+For Web-Applications, there are many SVG libraries for all the different frameworks and SVG icon sets.
+They all have the following in common:
+- There is either a `icon=` property or you can use the SVG directly, like ``
+- There is a `size` property (default is often 24)
+- There is `color` property
+
+So what you have is usually this:
+```html
+
+
+
+```
+
+The API of `SvgNode` matches what is the de-facto standard for SVG libraries, in a way that is compatible with JavaFX.
+
+#### Changelog
+
+> Version 2.0.0
+
+- Made all properties CSS styleable. This is especially interesting together with CSS transitions.
+- Renamed `svgColor` to `color`. This is a breaking change. See the naming above why this was done.
+
+> Version 1.0.0
+
+- Initial release
diff --git a/pom.xml b/pom.xml
index a0f8d96..c42f3b4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
tools.maran
svgnode
- 1.0.0
+ 2.0.0
jar
SVGNode
@@ -30,7 +30,7 @@
org.junit.jupiter
junit-jupiter
- 6.1.0
+ 6.1.1
test
diff --git a/src/main/java/tools/maran/svgnode/SvgNode.java b/src/main/java/tools/maran/svgnode/SvgNode.java
index 5a112a7..0d0ce88 100644
--- a/src/main/java/tools/maran/svgnode/SvgNode.java
+++ b/src/main/java/tools/maran/svgnode/SvgNode.java
@@ -1,11 +1,18 @@
package tools.maran.svgnode;
-import javafx.beans.property.DoubleProperty;
-import javafx.beans.property.ObjectProperty;
-import javafx.beans.property.SimpleDoubleProperty;
-import javafx.beans.property.SimpleObjectProperty;
-import javafx.beans.property.SimpleStringProperty;
-import javafx.beans.property.StringProperty;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javafx.css.CssMetaData;
+import javafx.css.Styleable;
+import javafx.css.StyleableDoubleProperty;
+import javafx.css.StyleableObjectProperty;
+import javafx.css.StyleableProperty;
+import javafx.css.StyleableStringProperty;
+import javafx.css.converter.PaintConverter;
+import javafx.css.converter.SizeConverter;
+import javafx.css.converter.StringConverter;
import javafx.scene.Parent;
import javafx.scene.layout.Background;
import javafx.scene.layout.Region;
@@ -18,25 +25,35 @@
///
/// ```java
/// SvgNode icon = new SvgNode("M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z", 32);
-/// icon.setSvgColor(Color.RED);
+/// icon.setColor(Color.RED);
/// ```
///
/// # FXML usage
///
/// ```xml
-///
+///
/// ```
///
/// ## Extended FXML usage from a constant (String or Enum)
///
/// ```xml
-///
+///
///
///
///
///
/// ```
///
+/// # CSS
+///
+/// ```css
+/// .svg-node {
+/// -fx-path: "M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z";
+/// -fx-size: 24;
+/// -fx-color: red;
+/// }
+/// ```
+///
/// @author Marius Hanl
public class SvgNode extends Parent {
@@ -45,9 +62,9 @@ public class SvgNode extends Parent {
private final SvgContent svgContent;
- private StringProperty path;
- private DoubleProperty size;
- private ObjectProperty svgColor;
+ private StyleableStringProperty path;
+ private StyleableDoubleProperty size;
+ private StyleableObjectProperty color;
/// Creates an empty {@code SvgNode} with no SVG path and the default fit dimensions.
public SvgNode() {
@@ -77,14 +94,32 @@ public SvgNode(String path, double size) {
/// The SVG path content string (e.g. `"M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"`).
///
+ /// Can be set in CSS with `-fx-path`.
+ ///
/// @return the path property
- public final StringProperty pathProperty() {
+ /// @defaultValue ""
+ public final StyleableStringProperty pathProperty() {
if (path == null) {
- path = new SimpleStringProperty(this, "path", DEFAULT_PATH) {
+ path = new StyleableStringProperty(DEFAULT_PATH) {
@Override
protected void invalidated() {
svgContent.updatePath(get(), getSize());
}
+
+ @Override
+ public CssMetaData extends Styleable, String> getCssMetaData() {
+ return StyleableProperties.PATH;
+ }
+
+ @Override
+ public Object getBean() {
+ return SvgNode.this;
+ }
+
+ @Override
+ public String getName() {
+ return "path";
+ }
};
}
return path;
@@ -103,15 +138,32 @@ public final void setPath(String value) {
/// The SVG path is rasterized uniformly to fit within a square of this size
/// while preserving its original aspect ratio.
///
+ /// Can be set in CSS with `-fx-size`.
+ ///
/// @return the size property
/// @defaultValue 24
- public final DoubleProperty sizeProperty() {
+ public final StyleableDoubleProperty sizeProperty() {
if (size == null) {
- size = new SimpleDoubleProperty(this, "size", DEFAULT_SIZE) {
+ size = new StyleableDoubleProperty(DEFAULT_SIZE) {
@Override
protected void invalidated() {
svgContent.updateRasterizedSize(get());
}
+
+ @Override
+ public CssMetaData extends Styleable, Number> getCssMetaData() {
+ return StyleableProperties.SIZE;
+ }
+
+ @Override
+ public Object getBean() {
+ return SvgNode.this;
+ }
+
+ @Override
+ public String getName() {
+ return "size";
+ }
};
}
return size;
@@ -127,26 +179,44 @@ public final void setSize(double value) {
/// The fill color of the SVG shape itself.
///
- /// @return the svgColor property
- public final ObjectProperty svgColorProperty() {
- if (svgColor == null) {
- svgColor = new SimpleObjectProperty<>(this, "svgColor") {
+ /// Can be set in CSS with `-fx-color`.
+ ///
+ /// @return the color property
+ /// @defaultValue null
+ public final StyleableObjectProperty colorProperty() {
+ if (color == null) {
+ color = new StyleableObjectProperty<>(null) {
@Override
protected void invalidated() {
final Paint color = get();
svgContent.setBackground(color == null ? null : Background.fill(color));
}
+
+ @Override
+ public CssMetaData extends Styleable, Paint> getCssMetaData() {
+ return StyleableProperties.COLOR;
+ }
+
+ @Override
+ public Object getBean() {
+ return SvgNode.this;
+ }
+
+ @Override
+ public String getName() {
+ return "color";
+ }
};
}
- return svgColor;
+ return color;
}
- public final Paint getSvgColor() {
- return svgColor == null ? null : svgColor.get();
+ public final Paint getColor() {
+ return color == null ? null : color.get();
}
- public final void setSvgColor(Paint value) {
- svgColorProperty().set(value);
+ public final void setColor(Paint value) {
+ colorProperty().set(value);
}
@Override
@@ -193,13 +263,80 @@ protected double computePrefWidth(double height) {
protected void layoutChildren() {
double nodeSize = getSize();
- double width = svgContent.prefWidth(nodeSize);
- double height = svgContent.prefHeight(nodeSize);
+ double width = svgContent.svgWidth;
+ double height = svgContent.svgHeight;
double x = (nodeSize - width) / 2;
double y = (nodeSize - height) / 2;
svgContent.resizeRelocate(x, y, width, height);
}
+ /// Gets the {@code CssMetaData} associated with this class.
+ /// Includes the {@code CssMetaData} of its superclasses.
+ ///
+ /// @return the {@code CssMetaData}
+ public static List> getClassCssMetaData() {
+ return StyleableProperties.STYLEABLES;
+ }
+
+ @Override
+ public List> getCssMetaData() {
+ return getClassCssMetaData();
+ }
+
+ private static class StyleableProperties {
+
+ private static final CssMetaData PATH = new CssMetaData<>("-fx-path",
+ StringConverter.getInstance(), DEFAULT_PATH) {
+ @Override
+ public boolean isSettable(SvgNode node) {
+ return node.path == null || !node.path.isBound();
+ }
+
+ @Override
+ public StyleableProperty getStyleableProperty(SvgNode node) {
+ return node.pathProperty();
+ }
+ };
+
+ private static final CssMetaData SIZE = new CssMetaData<>("-fx-size",
+ SizeConverter.getInstance(), DEFAULT_SIZE) {
+ @Override
+ public boolean isSettable(SvgNode node) {
+ return node.size == null || !node.size.isBound();
+ }
+
+ @Override
+ public StyleableProperty getStyleableProperty(SvgNode node) {
+ return node.sizeProperty();
+ }
+ };
+
+ private static final CssMetaData COLOR = new CssMetaData<>("-fx-color",
+ PaintConverter.getInstance()) {
+ @Override
+ public boolean isSettable(SvgNode node) {
+ return node.color == null || !node.color.isBound();
+ }
+
+ @Override
+ public StyleableProperty getStyleableProperty(SvgNode node) {
+ return node.colorProperty();
+ }
+ };
+
+ private static final List> STYLEABLES;
+
+ static {
+ List> metadata = Parent.getClassCssMetaData();
+ final List> styleables = new ArrayList<>(metadata.size() + 3);
+ styleables.addAll(metadata);
+ styleables.add(PATH);
+ styleables.add(SIZE);
+ styleables.add(COLOR);
+ STYLEABLES = Collections.unmodifiableList(styleables);
+ }
+ }
+
/// A [Region] that uses an [SVGPath] as its shape so that it can be styled and used as normal children.
///
/// @author Marius Hanl
@@ -260,16 +397,15 @@ private void calculateSvgSize(double rasterizedSize) {
final double w = svgPath.prefWidth(-1);
final double h = svgPath.prefHeight(-1);
- double finalW = rasterizedSize * w;
- double finalH = rasterizedSize * h;
- if (finalH > finalW) {
- svgWidth = Math.round(finalW / h);
+ if (h > w) {
+ double finalW = rasterizedSize * w;
+ svgWidth = snapSizeX(finalW / h);
svgHeight = rasterizedSize;
} else {
- svgHeight = Math.round(finalH / w);
+ double finalH = rasterizedSize * h;
+ svgHeight = snapSizeY(finalH / w);
svgWidth = rasterizedSize;
}
-
requestLayout();
}
diff --git a/src/main/resources/tools/maran/svgnode/svg.css b/src/main/resources/tools/maran/svgnode/svg.css
index 19ca575..3780a9e 100644
--- a/src/main/resources/tools/maran/svgnode/svg.css
+++ b/src/main/resources/tools/maran/svgnode/svg.css
@@ -1,3 +1,9 @@
+/*
+ * Based on modena.css.
+ * If the stylesheet ever changes, this might not be a good idea anymore.
+ * We style .svg directly so that CSS code by the developer will always override the properties.
+ * This should be treated as a sensible default.
+ */
.svg {
-fx-background-color: -fx-text-base-color;
}
diff --git a/src/test/java/tools/maran/svgnode/SvgNodeTest.java b/src/test/java/tools/maran/svgnode/SvgNodeTest.java
index 7b9a472..d2cdb18 100644
--- a/src/test/java/tools/maran/svgnode/SvgNodeTest.java
+++ b/src/test/java/tools/maran/svgnode/SvgNodeTest.java
@@ -56,7 +56,7 @@ static void initToolkit() {
@DisplayName("Changing path at runtime swaps the rendered shape")
void testChangePath() throws Exception {
SvgNode node = new SvgNode(SQUARE, 48);
- node.setSvgColor(Color.RED);
+ node.setColor(Color.RED);
WritableImage image = showAndSnapshot(node);
assertSvgDimensions(node, 48, 48);
@@ -78,7 +78,7 @@ void testChangePath() throws Exception {
@DisplayName("Changing size at runtime rescales the SVG content")
void testChangeSize() throws Exception {
SvgNode node = new SvgNode(SQUARE, 24);
- node.setSvgColor(Color.RED);
+ node.setColor(Color.RED);
WritableImage smallImage = showAndSnapshot(node);
assertSvgDimensions(node, 24, 24);
@@ -98,12 +98,12 @@ void testChangeSize() throws Exception {
@DisplayName("Changing SvgNode color at runtime updates rendered pixels")
void testColorChange() throws Exception {
SvgNode node = new SvgNode(SQUARE, 24);
- node.setSvgColor(Color.RED);
+ node.setColor(Color.RED);
WritableImage image = showAndSnapshot(node);
assertPixelsColor(image, Color.RED);
- node.setSvgColor(Color.GREEN);
+ node.setColor(Color.GREEN);
image = showAndSnapshot(node);
assertPixelsColor(image, Color.GREEN);
}
@@ -112,13 +112,13 @@ void testColorChange() throws Exception {
@DisplayName("Setting SVG color to null clears fill to transparent")
void testColorToNull() throws Exception {
SvgNode node = new SvgNode(SQUARE, 24);
- node.setSvgColor(Color.RED);
+ node.setColor(Color.RED);
WritableImage image = showAndSnapshot(node);
assertPixelsColor(image, Color.RED);
runOnFxThread(() -> {
- node.setSvgColor(null);
+ node.setColor(null);
return null;
});
@@ -142,7 +142,7 @@ void testDefaultAttributes() {
SvgNode node = new SvgNode();
assertEquals("", node.getPath());
- assertNull(node.getSvgColor());
+ assertNull(node.getColor());
assertEquals(24, node.getSize());
assertEquals(24, node.minWidth(-1));
@@ -161,11 +161,11 @@ void testDefaultAttributes() {
assertEquals("M 0 0z", node.getPath());
// Initialize property but do not use yet.
- ObjectProperty colorProperty = node.svgColorProperty();
- assertNull(node.getSvgColor());
+ ObjectProperty colorProperty = node.colorProperty();
+ assertNull(node.getColor());
colorProperty.set(Color.RED);
- assertEquals(Color.RED, node.getSvgColor());
+ assertEquals(Color.RED, node.getColor());
// Initialize property but do not use yet.
DoubleProperty sizeProperty = node.sizeProperty();
@@ -187,7 +187,7 @@ void testDefaultAttributes() {
@DisplayName("Horizontal 2:1 rect scales width to size and centers vertically")
void testHorizontalRect() throws Exception {
SvgNode node = new SvgNode(RECT_HORIZONTAL, 48);
- node.setSvgColor(Color.RED);
+ node.setColor(Color.RED);
WritableImage image = showAndSnapshot(node);
@@ -201,7 +201,7 @@ void testHorizontalRect() throws Exception {
@DisplayName("Square SVG at 32px renders solid blue")
void testSquareBlue32() throws Exception {
SvgNode node = new SvgNode(SQUARE, 32);
- node.setSvgColor(Color.BLUE);
+ node.setColor(Color.BLUE);
WritableImage image = showAndSnapshot(node);
@@ -224,7 +224,7 @@ void testSquareDefault() throws Exception {
@DisplayName("Square at 64px renders solid green")
void testSquareGreen64() throws Exception {
SvgNode node = new SvgNode(SQUARE, 64);
- node.setSvgColor(Color.GREEN);
+ node.setColor(Color.GREEN);
WritableImage image = showAndSnapshot(node);
@@ -236,7 +236,7 @@ void testSquareGreen64() throws Exception {
@DisplayName("Square at 48px renders solid red")
void testSquareRed48() throws Exception {
SvgNode node = new SvgNode(SQUARE, 48);
- node.setSvgColor(Color.RED);
+ node.setColor(Color.RED);
WritableImage image = showAndSnapshot(node);
@@ -248,7 +248,7 @@ void testSquareRed48() throws Exception {
@DisplayName("Vertical 1:2 rect scales height to size and centers horizontally")
void testVerticalRect() throws Exception {
SvgNode node = new SvgNode(RECT_VERTICAL, 48);
- node.setSvgColor(Color.BLUE);
+ node.setColor(Color.BLUE);
WritableImage image = showAndSnapshot(node);
@@ -279,9 +279,12 @@ private static void assertPixelsColor(WritableImage image, Color expected, Recta
for (int y = y1; y < y2; y++) {
for (int x = x1; x < x2; x++) {
Color actual = reader.getColor(x, y);
- assertEquals(expected.getRed(), actual.getRed(), EPS, "red mismatch at (%d,%d)".formatted(x, y));
- assertEquals(expected.getGreen(), actual.getGreen(), EPS, "green mismatch at (%d,%d)".formatted(x, y));
- assertEquals(expected.getBlue(), actual.getBlue(), EPS, "blue mismatch at (%d,%d)".formatted(x, y));
+ assertEquals(expected.getRed(), actual.getRed(), EPS,
+ "red mismatch at (%d,%d)".formatted(x, y));
+ assertEquals(expected.getGreen(), actual.getGreen(), EPS,
+ "green mismatch at (%d,%d)".formatted(x, y));
+ assertEquals(expected.getBlue(), actual.getBlue(), EPS,
+ "blue mismatch at (%d,%d)".formatted(x, y));
assertEquals(expected.getOpacity(), actual.getOpacity(), EPS,
"opacity mismatch at (%d,%d)".formatted(x, y));
}
@@ -307,7 +310,7 @@ private static void assertSvgDimensions(SvgNode node, double expectedWidth, doub
assertEquals(expectedHeight, widthHeightDimensions[5], EPS, "svg maxHeight");
}
- private static Scene createScene(SvgNode node) {
+ private static Scene useScene(SvgNode node) {
if (node.getScene() != null) {
return node.getScene();
}
@@ -333,7 +336,7 @@ private static T runOnFxThread(Callable action) throws Exception {
private static WritableImage showAndSnapshot(SvgNode node) throws Exception {
return runOnFxThread(() -> {
- Scene scene = createScene(node);
+ Scene scene = useScene(node);
return scene.snapshot(null);
});
}
diff --git a/src/test/java/tools/maran/svgnode/manual/SamplerApp.java b/src/test/java/tools/maran/svgnode/manual/SamplerApp.java
index 7ab61c4..a8bde23 100644
--- a/src/test/java/tools/maran/svgnode/manual/SamplerApp.java
+++ b/src/test/java/tools/maran/svgnode/manual/SamplerApp.java
@@ -29,12 +29,17 @@ public void start(Stage stage) {
explorerTab.setContent(new SvgLibraryExplorer().getView());
explorerTab.setClosable(false);
- TabPane tabPane = new TabPane(samplerTab, explorerTab);
- Scene scene = new Scene(new StackPane(tabPane), 800, 600);
+ TabPane tabPane = new TabPane();
+ tabPane.getSelectionModel().selectedItemProperty().addListener(_ -> setTitle(stage, tabPane));
+ tabPane.getTabs().setAll(samplerTab, explorerTab);
+ Scene scene = new Scene(new StackPane(tabPane), 800, 600);
scene.getStylesheets().add(getClass().getResource("sampler.css").toExternalForm());
- stage.setTitle("SvgNode – Sampler App");
stage.setScene(scene);
stage.show();
}
+
+ private void setTitle(Stage stage, TabPane tabPane) {
+ stage.setTitle(tabPane.getSelectionModel().getSelectedItem().getText());
+ }
}
diff --git a/src/test/java/tools/maran/svgnode/manual/SvgLibraryExplorer.java b/src/test/java/tools/maran/svgnode/manual/SvgLibraryExplorer.java
index dfec9d7..11555db 100644
--- a/src/test/java/tools/maran/svgnode/manual/SvgLibraryExplorer.java
+++ b/src/test/java/tools/maran/svgnode/manual/SvgLibraryExplorer.java
@@ -42,6 +42,7 @@
import tools.maran.svgnode.SvgNode;
/// An SVG explorer that displays all available SVGs from a library in a searchable, browsable grid.
+/// All SVGs are rendered with the [SvgNode].
///
/// Supported libraries:
/// - Material Design (Interface / Technology / World)
diff --git a/src/test/java/tools/maran/svgnode/manual/SvgNodeSampler.java b/src/test/java/tools/maran/svgnode/manual/SvgNodeSampler.java
index 897350f..62843f7 100644
--- a/src/test/java/tools/maran/svgnode/manual/SvgNodeSampler.java
+++ b/src/test/java/tools/maran/svgnode/manual/SvgNodeSampler.java
@@ -2,6 +2,7 @@
import java.nio.charset.StandardCharsets;
import java.util.Base64;
+import java.util.List;
import java.util.function.Consumer;
import javafx.geometry.Insets;
@@ -37,19 +38,36 @@ public class SvgNodeSampler {
private static final String STAR = "M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z";
private static final String HEART = "M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z";
- private static final String[] ALL_PATHS = { HOME, MAIL, FILTER, STAR, HEART };
- private static final String[] PATH_NAMES = { "Home", "Mail", "Filter", "Star", "Heart" };
- private static final Color[] COLORS = { Color.DODGERBLUE, Color.CRIMSON, Color.FORESTGREEN, Color.ORANGE,
- Color.MEDIUMPURPLE };
- private static final String[] COLOR_NAMES = { "Blue", "Red", "Green", "Orange", "Purple" };
- private static final Double[] SIZES = { 16d, 24d, 32d, 48d, 64d, 128d };
- private static final String[] SIZE_LABELS = { "16px", "24px", "32px", "48px", "64px", "128px" };
+ private static final List> PATHS = List.of(
+ new Named<>("Home", HOME),
+ new Named<>("Mail", MAIL),
+ new Named<>("Filter", FILTER),
+ new Named<>("Star", STAR),
+ new Named<>("Heart", HEART)
+ );
+
+ private static final List> COLORS = List.of(
+ new Named<>("Blue", Color.DODGERBLUE),
+ new Named<>("Red", Color.CRIMSON),
+ new Named<>("Green", Color.FORESTGREEN),
+ new Named<>("Orange", Color.ORANGE),
+ new Named<>("Purple", Color.MEDIUMPURPLE)
+ );
+
+ private static final List> SIZES = List.of(
+ new Named<>("16px", 16d),
+ new Named<>("24px", 24d),
+ new Named<>("32px", 32d),
+ new Named<>("48px", 48d),
+ new Named<>("64px", 64d),
+ new Named<>("128px", 128d)
+ );
private final VBox view;
public SvgNodeSampler() {
- view = new VBox(8);
- view.setPadding(new Insets(8));
+ view = new VBox(4);
+ view.setPadding(new Insets(4));
view.setAlignment(Pos.TOP_LEFT);
addLabels();
@@ -60,21 +78,6 @@ public SvgNodeSampler() {
addSvgResize();
}
- private void addLabels() {
- view.getChildren().add(sectionLabel("Labels"));
-
- Label label = new Label("SVG with text", new SvgNode(HEART));
- Label label2 = new Label("Green SVG with green text (CSS)", new SvgNode(HEART));
- label2.getStylesheets().add(toBase64("""
- .label {
- -fx-text-base-color: green;
- -fx-text-fill: green;
- }
- """));
-
- view.getChildren().add(new HBox(4, label, label2));
- }
-
public Node getView() {
return view;
}
@@ -134,16 +137,42 @@ private void addButtonGraphics() {
view.getChildren().add(new HBox(4, buttonDark, buttonDark2));
}
+ private void addLabels() {
+ view.getChildren().add(sectionLabel("Label and SvgNode styled with CSS (+hover effect)"));
+
+ Label label = new Label("SVG with text", new SvgNode(HEART));
+
+ SvgNode svgNodeHover = new SvgNode();
+ svgNodeHover.getStylesheets().add(toBase64("""
+ .svg-node {
+ -fx-path: "M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z";
+ -fx-size: 24;
+ -fx-color: red;
+ }
+
+ .svg-node:hover {
+ -fx-path: "M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z";
+ -fx-size: 32;
+ -fx-color: blue;
+ }
+ """));
+
+ view.getChildren().add(new HBox(4, label, svgNodeHover));
+ }
+
private void addStaticSvgs() {
view.getChildren().addAll(new Separator(), sectionLabel("Static icons with Tooltip (24px)"));
HBox staticRow = new HBox(12);
staticRow.setAlignment(Pos.CENTER_LEFT);
- for (int i = 0; i < ALL_PATHS.length; i++) {
- SvgNode icon = new SvgNode(ALL_PATHS[i]);
- icon.setSvgColor(COLORS[i]);
- Tooltip.install(icon, new Tooltip(PATH_NAMES[i]));
- staticRow.getChildren().addAll(icon, new Label(PATH_NAMES[i]));
+ for (int i = 0; i < PATHS.size(); i++) {
+ Named path = PATHS.get(i);
+ Named color = COLORS.get(i);
+
+ SvgNode icon = new SvgNode(path.value());
+ icon.setColor(color.value());
+ Tooltip.install(icon, new Tooltip(path.label()));
+ staticRow.getChildren().addAll(icon, new Label(path.label()));
}
view.getChildren().add(staticRow);
}
@@ -152,9 +181,9 @@ private void addSvgColorChange() {
view.getChildren().addAll(new Separator(), sectionLabel("Runtime color change"));
SvgNode colorTarget = new SvgNode(HEART, 40);
- colorTarget.setSvgColor(Color.CRIMSON);
+ colorTarget.setColor(Color.CRIMSON);
- HBox toggleBar = createToggleBar(COLORS, COLOR_NAMES, 1, colorTarget::setSvgColor);
+ HBox toggleBar = createToggleBar(COLORS, 1, colorTarget::setColor);
view.getChildren().add(new HBox(12, colorTarget, toggleBar));
}
@@ -162,9 +191,9 @@ private void addSvgPathSwap() {
view.getChildren().addAll(new Separator(), sectionLabel("Runtime path swap"));
SvgNode swappable = new SvgNode(HOME, 32);
- swappable.setSvgColor(Color.DODGERBLUE);
+ swappable.setColor(Color.DODGERBLUE);
- HBox toggleBar = createToggleBar(ALL_PATHS, PATH_NAMES, 0, swappable::setPath);
+ HBox toggleBar = createToggleBar(PATHS, 0, swappable::setPath);
view.getChildren().add(new HBox(12, swappable, toggleBar));
}
@@ -172,20 +201,20 @@ private void addSvgResize() {
view.getChildren().addAll(new Separator(), sectionLabel("Runtime resize"));
SvgNode resizable = new SvgNode(STAR, 24);
- resizable.setSvgColor(Color.ORANGE);
+ resizable.setColor(Color.ORANGE);
- HBox toggleBar = createToggleBar(SIZES, SIZE_LABELS, 1, resizable::setSize);
- view.getChildren().add(new HBox(12, resizable, toggleBar));
+ HBox toggleBar = createToggleBar(SIZES, 1, resizable::setSize);
+ view.getChildren().add(new VBox(4, toggleBar, resizable));
}
private Button createButtonWithCss(String text, String css) {
- Button buttonLight = new Button(text, new SvgNode(HEART));
- buttonLight.setCursor(Cursor.HAND);
- buttonLight.getStylesheets().add(toBase64(css));
- return buttonLight;
+ Button button = new Button(text, new SvgNode(HEART));
+ button.setCursor(Cursor.HAND);
+ button.getStylesheets().add(toBase64(css));
+ return button;
}
- private HBox createToggleBar(T[] values, String[] labels, int defaultIndex, Consumer onSelect) {
+ private HBox createToggleBar(List> items, int defaultIndex, Consumer onSelect) {
HBox box = new HBox(0);
box.setAlignment(Pos.CENTER_LEFT);
@@ -197,14 +226,15 @@ private HBox createToggleBar(T[] values, String[] labels, int defaultIndex,
onSelect.accept((T) toggle.getUserData());
});
- for (int i = 0; i < values.length; i++) {
- ToggleButton btn = new ToggleButton(labels[i]);
- btn.setUserData(values[i]);
+ for (int i = 0; i < items.size(); i++) {
+ Named item = items.get(i);
+ ToggleButton btn = new ToggleButton(item.label());
+ btn.setUserData(item.value());
btn.setToggleGroup(group);
if (i == 0) {
btn.getStyleClass().add("left-pill");
- } else if (i == values.length - 1) {
+ } else if (i == items.size() - 1) {
btn.getStyleClass().add("right-pill");
} else {
btn.getStyleClass().add("center-pill");
@@ -226,4 +256,6 @@ private static Label sectionLabel(String text) {
private static String toBase64(String css) {
return "data:base64," + Base64.getUrlEncoder().encodeToString(css.getBytes(StandardCharsets.UTF_8));
}
+
+ private record Named(String label, T value) { }
}
diff --git a/src/test/resources/tools/maran/svgnode/manual/sampler.css b/src/test/resources/tools/maran/svgnode/manual/sampler.css
index cf6fad9..7baf151 100644
--- a/src/test/resources/tools/maran/svgnode/manual/sampler.css
+++ b/src/test/resources/tools/maran/svgnode/manual/sampler.css
@@ -141,6 +141,15 @@
-fx-border-width: 1 1 1 0.5;
}
+/* SvgNode Sampler */
+
+.header {
+ -fx-font-weight: bold;
+ -fx-font-size: 14px;
+}
+
+/* Svg Library Explorer */
+
.svg-button {
-fx-color: -surface;
-fx-background-color: -surface;
@@ -164,11 +173,6 @@
-fx-border-color: -border-hover;
}
-.header {
- -fx-font-weight: bold;
- -fx-font-size: 14px;
-}
-
.content-area {
-fx-background-color: -surface-alt;
-fx-background-radius: 8;