diff --git a/pom.xml b/pom.xml
index 7e08e84..95614a3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -15,6 +15,13 @@
1.0-SNAPSHOT
+
+
+ io.reactivex.rxjava2
+ rxjava
+ 2.2.7
+
+
org.slf4j
slf4j-api
diff --git a/src/main/java/ovh/gasser/rxshapes/App.java b/src/main/java/ovh/gasser/rxshapes/App.java
new file mode 100644
index 0000000..c7157fe
--- /dev/null
+++ b/src/main/java/ovh/gasser/rxshapes/App.java
@@ -0,0 +1,43 @@
+package ovh.gasser.rxshapes;
+
+import ovh.gasser.rxshapes.shapes.SCollection;
+import ovh.gasser.rxshapes.shapes.SRectangle;
+import ovh.gasser.rxshapes.shapes.Shape;
+import ovh.gasser.rxshapes.ui.ShapesView;
+
+import javax.swing.*;
+import java.awt.*;
+
+public class App {
+
+ public static final Dimension WIN_SIZE = new Dimension(800, 600);
+ private Shape model;
+
+ private App() throws HeadlessException {
+ final JFrame frame = new JFrame("Reactive shapes");
+ frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+ buildModel();
+ final ShapesView view = new ShapesView(this.model);
+ view.setPreferredSize(WIN_SIZE);
+ frame.getContentPane().add(view, BorderLayout.CENTER);
+ frame.setContentPane(view);
+ frame.pack();
+ frame.setVisible(true);
+ }
+
+ private void buildModel() {
+ model = SCollection.of(
+ SRectangle.create(10, 10, 40, 60, Color.RED),
+ SRectangle.create(70, 10, 40, 60),
+ SCollection.of(
+ SRectangle.create(100, 200, 40, 60, Color.MAGENTA),
+ SRectangle.create(150, 200, 40, 60, Color.MAGENTA)
+ )
+ );
+ }
+
+ public static void main(String[] args) {
+ SwingUtilities.invokeLater(App::new);
+ }
+
+}
diff --git a/src/main/java/ovh/gasser/rxshapes/HTMLExporter.java b/src/main/java/ovh/gasser/rxshapes/HTMLExporter.java
new file mode 100644
index 0000000..9059e60
--- /dev/null
+++ b/src/main/java/ovh/gasser/rxshapes/HTMLExporter.java
@@ -0,0 +1,39 @@
+package ovh.gasser.rxshapes;
+
+import ovh.gasser.rxshapes.shapes.SCollection;
+import ovh.gasser.rxshapes.shapes.SRectangle;
+import ovh.gasser.rxshapes.ui.html.*;
+
+import java.awt.*;
+import java.io.FileNotFoundException;
+import java.io.PrintWriter;
+
+public class HTMLExporter {
+
+ private final SCollection model;
+
+ private HTMLExporter() throws FileNotFoundException {
+ model = SCollection.of(
+ SRectangle.create(10, 10, 40, 60, Color.RED),
+ SRectangle.create(70, 10, 40, 60),
+ SCollection.of(
+ SRectangle.create(100, 200, 40, 60, Color.MAGENTA),
+ SRectangle.create(150, 200, 40, 60, Color.MAGENTA)
+ )
+ );
+ export();
+ }
+
+ private void export() throws FileNotFoundException {
+ try (final PrintWriter html = new PrintWriter("out.html")) {
+ try (final PrintWriter css = new PrintWriter("style.css")) {
+ HTMLDraftman draftman = new HTMLDraftman(html, css);
+ draftman.generateHTML(this.model);
+ }
+ }
+ }
+
+ public static void main(String[] args) throws FileNotFoundException {
+ new HTMLExporter().export();
+ }
+}
diff --git a/src/main/java/ovh/gasser/rxshapes/Selection.java b/src/main/java/ovh/gasser/rxshapes/Selection.java
new file mode 100644
index 0000000..300b3b4
--- /dev/null
+++ b/src/main/java/ovh/gasser/rxshapes/Selection.java
@@ -0,0 +1,24 @@
+package ovh.gasser.rxshapes;
+
+import ovh.gasser.rxshapes.attributes.SelectionAttributes;
+import ovh.gasser.rxshapes.shapes.Shape;
+
+public class Selection {
+
+ private final SelectionAttributes attributes;
+ public final Shape shape;
+
+ public Selection(final Shape shape, boolean selected) {
+ this(shape);
+ attributes.selected = selected;
+ }
+
+ private Selection(final Shape shape) {
+ attributes = (SelectionAttributes) shape.getAttributes(SelectionAttributes.ID);
+ this.shape = shape;
+ }
+
+ public void unselect() {
+ attributes.selected = false;
+ }
+}
diff --git a/src/main/java/ovh/gasser/rxshapes/ShapeVisitor.java b/src/main/java/ovh/gasser/rxshapes/ShapeVisitor.java
new file mode 100644
index 0000000..c5776cf
--- /dev/null
+++ b/src/main/java/ovh/gasser/rxshapes/ShapeVisitor.java
@@ -0,0 +1,10 @@
+package ovh.gasser.rxshapes;
+
+import ovh.gasser.rxshapes.shapes.SCollection;
+import ovh.gasser.rxshapes.shapes.SRectangle;
+
+public interface ShapeVisitor {
+ void visitRectangle(SRectangle sRectangle);
+
+ void visitCollection(SCollection collection);
+}
diff --git a/src/main/java/ovh/gasser/rxshapes/attributes/Attributes.java b/src/main/java/ovh/gasser/rxshapes/attributes/Attributes.java
new file mode 100644
index 0000000..e3579fd
--- /dev/null
+++ b/src/main/java/ovh/gasser/rxshapes/attributes/Attributes.java
@@ -0,0 +1,5 @@
+package ovh.gasser.rxshapes.attributes;
+
+public interface Attributes {
+ String getID();
+}
diff --git a/src/main/java/ovh/gasser/rxshapes/attributes/ColorAttributes.java b/src/main/java/ovh/gasser/rxshapes/attributes/ColorAttributes.java
new file mode 100644
index 0000000..f8f929b
--- /dev/null
+++ b/src/main/java/ovh/gasser/rxshapes/attributes/ColorAttributes.java
@@ -0,0 +1,33 @@
+package ovh.gasser.rxshapes.attributes;
+
+import java.awt.*;
+
+public class ColorAttributes implements Attributes {
+ public static final String ID = "COLOR_ATTRS";
+ public final boolean filled;
+ public final boolean stroked;
+ public final Color filledColor;
+ public final Color strokedColor;
+
+ public ColorAttributes(boolean filled, boolean stroked, Color filledColor, Color strokedColor) {
+ this.filled = filled;
+ this.stroked = stroked;
+ this.filledColor = filledColor;
+ this.strokedColor = strokedColor;
+ }
+
+ @Override
+ public String toString() {
+ return "ColorAttributes{" +
+ "filled=" + filled +
+ ", stroked=" + stroked +
+ ", filledColor=" + filledColor +
+ ", strokedColor=" + strokedColor +
+ '}';
+ }
+
+ @Override
+ public String getID() {
+ return ID;
+ }
+}
diff --git a/src/main/java/ovh/gasser/rxshapes/attributes/SelectionAttributes.java b/src/main/java/ovh/gasser/rxshapes/attributes/SelectionAttributes.java
new file mode 100644
index 0000000..56d1059
--- /dev/null
+++ b/src/main/java/ovh/gasser/rxshapes/attributes/SelectionAttributes.java
@@ -0,0 +1,11 @@
+package ovh.gasser.rxshapes.attributes;
+
+public class SelectionAttributes implements Attributes {
+ public static final String ID = "SELECTION_ATTRS";
+ public boolean selected;
+
+ @Override
+ public String getID() {
+ return ID;
+ }
+}
diff --git a/src/main/java/ovh/gasser/rxshapes/shapes/AbstractShape.java b/src/main/java/ovh/gasser/rxshapes/shapes/AbstractShape.java
new file mode 100644
index 0000000..e235cf5
--- /dev/null
+++ b/src/main/java/ovh/gasser/rxshapes/shapes/AbstractShape.java
@@ -0,0 +1,51 @@
+package ovh.gasser.rxshapes.shapes;
+
+import ovh.gasser.rxshapes.attributes.Attributes;
+
+import java.awt.*;
+import java.util.Map;
+import java.util.TreeMap;
+
+public abstract class AbstractShape implements Shape {
+
+ private Map attributes = new TreeMap<>();
+ private Rectangle bounds;
+
+ AbstractShape() {
+ this(null);
+ }
+
+ AbstractShape(Rectangle bounds) {
+ this.bounds = bounds;
+ }
+
+ @Override
+ public Attributes getAttributes(String key) {
+ return attributes.get(key);
+ }
+
+ @Override
+ public void addAttributes(Attributes attrs) {
+ attributes.put(attrs.getID(), attrs);
+ }
+
+ @Override
+ public void setLoc(Point newLoc) {
+ getBounds().setLocation(newLoc);
+ }
+
+ @Override
+ public void translate(int dx, int dy) {
+ getBounds().translate(dx, dy);
+ }
+
+ @Override
+ public Rectangle getBounds() {
+ return this.bounds;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("x=%d, y=%d, width=%d, height=%d", bounds.x, bounds.y, bounds.width, bounds.height);
+ }
+}
diff --git a/src/main/java/ovh/gasser/rxshapes/shapes/SCollection.java b/src/main/java/ovh/gasser/rxshapes/shapes/SCollection.java
new file mode 100644
index 0000000..52f12e8
--- /dev/null
+++ b/src/main/java/ovh/gasser/rxshapes/shapes/SCollection.java
@@ -0,0 +1,65 @@
+package ovh.gasser.rxshapes.shapes;
+
+import ovh.gasser.rxshapes.App;
+import ovh.gasser.rxshapes.attributes.SelectionAttributes;
+import ovh.gasser.rxshapes.ShapeVisitor;
+import ovh.gasser.rxshapes.util.Streamable;
+
+import java.awt.*;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Spliterator;
+
+public class SCollection extends AbstractShape implements Streamable {
+ private final List children;
+
+ private SCollection(Shape... shapes) {
+ this.children = List.of(shapes);
+ }
+
+ @Override
+ public void accept(ShapeVisitor visitor) {
+ visitor.visitCollection(this);
+ }
+
+ @Override
+ public Rectangle getBounds() {
+ try {
+ Rectangle bounds = children.get(0).getBounds();
+ for (Shape s : children) bounds = bounds.union(s.getBounds());
+ return bounds;
+ } catch (IndexOutOfBoundsException e){
+ // If the SCollection is empty, set the bounds to fill the window
+ return new Rectangle(App.WIN_SIZE);
+ }
+ }
+
+ @Override
+ public void setLoc(Point newLoc) {
+ final Point loc = getBounds().getLocation();
+ children.forEach(s -> s.translate(newLoc.x - loc.x, newLoc.y - loc.y));
+ }
+
+ @Override
+ public Iterator iterator() {
+ return children.iterator();
+ }
+
+ @Override
+ public Spliterator spliterator() {
+ return children.spliterator();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder("SCollection{");
+ children.forEach(obj -> sb.append(obj).append(", "));
+ return sb.append("}").toString();
+ }
+
+ public static SCollection of(Shape ...shapes) {
+ SCollection collection = new SCollection(shapes);
+ collection.addAttributes(new SelectionAttributes());
+ return collection;
+ }
+}
diff --git a/src/main/java/ovh/gasser/rxshapes/shapes/SRectangle.java b/src/main/java/ovh/gasser/rxshapes/shapes/SRectangle.java
new file mode 100644
index 0000000..4f386c3
--- /dev/null
+++ b/src/main/java/ovh/gasser/rxshapes/shapes/SRectangle.java
@@ -0,0 +1,38 @@
+package ovh.gasser.rxshapes.shapes;
+
+import ovh.gasser.rxshapes.attributes.ColorAttributes;
+import ovh.gasser.rxshapes.attributes.SelectionAttributes;
+import ovh.gasser.rxshapes.ShapeVisitor;
+
+import java.awt.*;
+
+public class SRectangle extends AbstractShape {
+
+ private SRectangle(Rectangle rect) {
+ super(rect);
+ }
+
+ @Override
+ public void accept(ShapeVisitor visitor) {
+ visitor.visitRectangle(this);
+ }
+
+ @Override
+ public String toString() {
+ return "SRectangle{" +
+ super.toString() +
+ '}';
+ }
+
+ public static SRectangle create(int x, int y, int width, int height) {
+ SRectangle rect = new SRectangle(new Rectangle(x, y, width, height));
+ rect.addAttributes(new SelectionAttributes());
+ return rect;
+ }
+
+ public static SRectangle create(int x, int y, int width, int height, Color color) {
+ final SRectangle rect = create(x, y, width, height);
+ rect.addAttributes(new ColorAttributes(false, true, Color.BLACK, color));
+ return rect;
+ }
+}
diff --git a/src/main/java/ovh/gasser/rxshapes/shapes/Shape.java b/src/main/java/ovh/gasser/rxshapes/shapes/Shape.java
new file mode 100644
index 0000000..f1cdaae
--- /dev/null
+++ b/src/main/java/ovh/gasser/rxshapes/shapes/Shape.java
@@ -0,0 +1,15 @@
+package ovh.gasser.rxshapes.shapes;
+
+import ovh.gasser.rxshapes.ShapeVisitor;
+import ovh.gasser.rxshapes.attributes.Attributes;
+
+import java.awt.*;
+
+public interface Shape {
+ void accept(ShapeVisitor visitor);
+ void setLoc(Point newLoc);
+ void translate(int dx, int dy);
+ Attributes getAttributes(String key);
+ void addAttributes(Attributes attr);
+ Rectangle getBounds();
+}
diff --git a/src/main/java/ovh/gasser/rxshapes/ui/Controller.java b/src/main/java/ovh/gasser/rxshapes/ui/Controller.java
new file mode 100644
index 0000000..37db7cc
--- /dev/null
+++ b/src/main/java/ovh/gasser/rxshapes/ui/Controller.java
@@ -0,0 +1,62 @@
+package ovh.gasser.rxshapes.ui;
+
+import io.reactivex.Observable;
+import io.reactivex.disposables.Disposable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import ovh.gasser.rxshapes.Selection;
+import ovh.gasser.rxshapes.util.EventSource;
+import ovh.gasser.rxshapes.shapes.Shape;
+import ovh.gasser.rxshapes.shapes.SCollection;
+
+import java.awt.event.MouseEvent;
+import java.util.Optional;
+
+public class Controller {
+ private final Logger logger = LoggerFactory.getLogger(ShapesView.class);
+ private final Disposable mouseSub;
+ private Selection selection;
+
+ Controller(ShapesView view, Shape model) {
+ mouseSub = EventSource
+ .fromMouseEventsOf(view)
+ .subscribe(evt -> {
+ assert model instanceof SCollection;
+ SCollection sc = (SCollection) model;
+ switch (evt.getID()) {
+ case MouseEvent.MOUSE_PRESSED:
+ getTarget(evt, sc)
+ .ifPresentOrElse(
+ s -> selection = new Selection(s, true),
+ () -> {
+ if (selection != null) {
+ selection.unselect();
+ selection = null;
+ }
+ }
+ );
+ break;
+ case MouseEvent.MOUSE_DRAGGED:
+ handleMouseMoved(evt);
+ break;
+ }
+ view.repaint();
+ }, err -> logger.error("{}", err)
+ );
+ }
+
+ private Optional getTarget(MouseEvent evt, SCollection sc) {
+ return sc.stream()
+ .filter(s -> s.getBounds().contains(evt.getPoint()))
+ .findFirst();
+ }
+
+ private void handleMouseMoved(MouseEvent evt) {
+ if (selection != null) selection.shape.setLoc(evt.getPoint());
+ }
+
+ public void dispose() {
+ logger.info("Cleaning subscriptions...");
+ mouseSub.dispose();
+ }
+}
diff --git a/src/main/java/ovh/gasser/rxshapes/ui/ShapeDraftman.java b/src/main/java/ovh/gasser/rxshapes/ui/ShapeDraftman.java
new file mode 100644
index 0000000..2103ece
--- /dev/null
+++ b/src/main/java/ovh/gasser/rxshapes/ui/ShapeDraftman.java
@@ -0,0 +1,59 @@
+package ovh.gasser.rxshapes.ui;
+
+import ovh.gasser.rxshapes.ShapeVisitor;
+import ovh.gasser.rxshapes.shapes.Shape;
+import ovh.gasser.rxshapes.attributes.ColorAttributes;
+import ovh.gasser.rxshapes.attributes.SelectionAttributes;
+import ovh.gasser.rxshapes.shapes.SCollection;
+import ovh.gasser.rxshapes.shapes.SRectangle;
+
+import java.awt.*;
+
+public class ShapeDraftman implements ShapeVisitor {
+
+ private static final ColorAttributes DEFAULT_COLOR_ATTRIBUTES =
+ new ColorAttributes(false, true, Color.BLACK, Color.BLACK);
+ private Graphics2D g2d;
+
+
+ public ShapeDraftman(Graphics graph) {
+ this.g2d = (Graphics2D) graph;
+ }
+
+ @Override
+ public void visitRectangle(SRectangle rect) {
+ Rectangle r = rect.getBounds();
+ ColorAttributes colAttrs = (ColorAttributes) rect.getAttributes(ColorAttributes.ID);
+ if (colAttrs == null){
+ colAttrs = DEFAULT_COLOR_ATTRIBUTES;
+ }
+ if (colAttrs.filled) {
+ this.g2d.setColor(colAttrs.filledColor);
+ this.g2d.fillRect(r.x, r.y, r.width, r.height);
+ }
+ if (colAttrs.stroked) {
+ this.g2d.setColor(colAttrs.strokedColor);
+ this.g2d.drawRect(r.x, r.y, r.width, r.height);
+ }
+ drawHandlerIfSelected(rect);
+ }
+
+ @Override
+ public void visitCollection(SCollection collection) {
+ for (Shape s: collection) {
+ s.accept(this);
+ }
+ drawHandlerIfSelected(collection);
+ }
+
+ private void drawHandlerIfSelected(Shape s) {
+ SelectionAttributes selAttrs = (SelectionAttributes) s.getAttributes(SelectionAttributes.ID);
+ if ((selAttrs != null) && (selAttrs.selected)){
+ Rectangle bounds = s.getBounds();
+ this.g2d.setColor(Color.RED);
+ this.g2d.drawRect(bounds.x - 5, bounds.y - 5, 5, 5);
+ this.g2d.drawRect(bounds.x + bounds.width, bounds.y + bounds.height, 5, 5);
+ }
+ }
+
+}
diff --git a/src/main/java/ovh/gasser/rxshapes/ui/ShapesView.java b/src/main/java/ovh/gasser/rxshapes/ui/ShapesView.java
new file mode 100644
index 0000000..710cbd2
--- /dev/null
+++ b/src/main/java/ovh/gasser/rxshapes/ui/ShapesView.java
@@ -0,0 +1,29 @@
+package ovh.gasser.rxshapes.ui;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import ovh.gasser.rxshapes.ShapeVisitor;
+import ovh.gasser.rxshapes.shapes.Shape;
+
+import javax.swing.*;
+import java.awt.*;
+
+public class ShapesView extends JPanel {
+ final Logger logger = LoggerFactory.getLogger(ShapesView.class);
+
+ private final Shape model;
+ private final Controller controller;
+ private ShapeVisitor draftman;
+
+ public ShapesView(Shape model) {
+ this.model = model;
+ this.controller = new Controller(this, model);
+ }
+
+ @Override
+ protected void paintComponent(Graphics g) {
+ super.paintComponent(g);
+ this.draftman = new ShapeDraftman(g);
+ model.accept(draftman);
+ }
+}
diff --git a/src/main/java/ovh/gasser/rxshapes/ui/html/HTMLDraftman.java b/src/main/java/ovh/gasser/rxshapes/ui/html/HTMLDraftman.java
new file mode 100644
index 0000000..4808c08
--- /dev/null
+++ b/src/main/java/ovh/gasser/rxshapes/ui/html/HTMLDraftman.java
@@ -0,0 +1,85 @@
+package ovh.gasser.rxshapes.ui.html;
+
+import ovh.gasser.rxshapes.ShapeVisitor;
+import ovh.gasser.rxshapes.attributes.ColorAttributes;
+import ovh.gasser.rxshapes.shapes.SCollection;
+import ovh.gasser.rxshapes.shapes.SRectangle;
+
+import java.awt.*;
+import java.io.PrintWriter;
+
+public class HTMLDraftman implements ShapeVisitor {
+
+ private static final String HEADER_TEMPLATE = "" +
+ "" +
+ "" +
+ " " +
+ " " +
+ " Reactive shapes HTML" +
+ "" +
+ "";
+
+ private static final String FOOTER_TEMPLATE = "";
+
+ private PrintWriter htmlOutput;
+ private PrintWriter cssOutput;
+
+ public HTMLDraftman(PrintWriter htmlOutput, PrintWriter cssOutput) {
+ this.htmlOutput = htmlOutput;
+ this.cssOutput = cssOutput;
+ }
+
+ @Override
+ public void visitRectangle(SRectangle rect) {
+ htmlOutput.println("");
+ cssOutput.println("#rec" + rect.hashCode() + "{ ");
+ cssOutput.println("position:absolute;");
+ cssOutput.println("top:" + rect.getBounds().y + "px;");
+ cssOutput.println("left:" + rect.getBounds().x + "px;");
+ cssOutput.println("width:" + rect.getBounds().width + "px;");
+ cssOutput.println("height:" + rect.getBounds().height + "px;");
+ cssOutput.println(this.attributesToCss(rect) + " }");
+ }
+
+ private String attributesToCss(SRectangle rect) {
+ ColorAttributes attrs = (ColorAttributes) rect.getAttributes(ColorAttributes.ID);
+ String strokedColor = "#ffffff";
+ String filledColor = "#ffffff";
+
+ if (attrs != null && attrs.filledColor != null){
+ filledColor = formatCSSColor(attrs.filledColor);
+ }
+ if (attrs != null && attrs.strokedColor != null){
+ strokedColor = formatCSSColor(attrs.strokedColor);
+ }
+ if(attrs != null && attrs.stroked && attrs.filled){
+ return "background: " + filledColor + ";border:1px solid " + strokedColor + ";";
+ }
+ if (attrs != null && attrs.stroked){
+ return "background:#ffffff;border:1px solid "+strokedColor+";";
+ }
+ if (attrs != null && attrs.filled) {
+ return "background: "+filledColor+";";
+ }
+
+ return "";
+ }
+
+ private String formatCSSColor(Color col) {
+ final int r = col.getRed();
+ final int g = col.getGreen();
+ final int b = col.getBlue();
+ return String.format("#%02x%02x%02x", r, g, b);
+ }
+
+ @Override
+ public void visitCollection(SCollection collection) {
+ collection.stream().forEach(shape -> shape.accept(this));
+ }
+
+ public void generateHTML(SCollection model) {
+ htmlOutput.println(HEADER_TEMPLATE);
+ visitCollection(model);
+ htmlOutput.println(FOOTER_TEMPLATE);
+ }
+}
diff --git a/src/main/java/ovh/gasser/rxshapes/util/EventSource.java b/src/main/java/ovh/gasser/rxshapes/util/EventSource.java
new file mode 100644
index 0000000..58a93c8
--- /dev/null
+++ b/src/main/java/ovh/gasser/rxshapes/util/EventSource.java
@@ -0,0 +1,29 @@
+package ovh.gasser.rxshapes.util;
+
+import io.reactivex.Observable;
+
+import java.awt.*;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.concurrent.Future;
+
+public class EventSource {
+ public static Observable fromMouseEventsOf(final Component component) {
+ return Observable.create(emitter -> {
+ final MouseAdapter adapter = new MouseAdapter() {
+ @Override
+ public void mousePressed(MouseEvent e) {
+ emitter.onNext(e);
+ }
+
+ @Override
+ public void mouseDragged(MouseEvent e) {
+ emitter.onNext(e);
+ }
+ };
+ component.addMouseMotionListener(adapter);
+ component.addMouseListener(adapter);
+ emitter.setCancellable(() -> component.removeMouseListener(adapter));
+ });
+ }
+}
diff --git a/src/main/java/ovh/gasser/rxshapes/util/Streamable.java b/src/main/java/ovh/gasser/rxshapes/util/Streamable.java
new file mode 100644
index 0000000..afd2835
--- /dev/null
+++ b/src/main/java/ovh/gasser/rxshapes/util/Streamable.java
@@ -0,0 +1,10 @@
+package ovh.gasser.rxshapes.util;
+
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+public interface Streamable extends Iterable {
+ default Stream stream() {
+ return StreamSupport.stream(this.spliterator(), false);
+ }
+}
diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml
new file mode 100644
index 0000000..c278dee
--- /dev/null
+++ b/src/main/resources/logback.xml
@@ -0,0 +1,13 @@
+
+
+ true
+
+ %d{HH:mm:ss.SSS} %highlight(%-5level) [%10thread] %cyan(%-40logger{36}) - %msg%n
+
+
+
+
+
+
+
+