[Feature] New Layout Engine

This commit is contained in:
Frank 2022-07-14 22:07:40 +02:00
parent 42fed7b3a0
commit f89035f9f3
12 changed files with 745 additions and 0 deletions

View file

@ -0,0 +1,81 @@
package org.betterx.ui.layout.components;
import org.betterx.ui.layout.components.input.MouseEvent;
import org.betterx.ui.layout.components.render.ComponentRenderer;
import org.betterx.ui.layout.values.DynamicSize;
import org.betterx.ui.layout.values.Rectangle;
public abstract class Component<R extends ComponentRenderer> implements ComponentWithBounds {
protected final R renderer;
protected final DynamicSize width;
protected final DynamicSize height;
protected Rectangle relativeBounds;
public Component(DynamicSize width, DynamicSize height, R renderer) {
this.width = width.attachComponent(this::getContentWidth);
this.height = height.attachComponent(this::getContentHeight);
this.renderer = renderer;
}
protected int updateContainerWidth(int containerWidth) {
return width.setCalculatedSize(containerWidth);
}
protected int updateContainerHeight(int containerHeight) {
return height.setCalculatedSize(containerHeight);
}
void setRelativeBounds(int left, int top) {
relativeBounds = new Rectangle(left, top, width.calculatedSize(), height.calculatedSize());
}
public Rectangle getRelativeBounds() {
return relativeBounds;
}
public abstract int getContentWidth();
public abstract int getContentHeight();
public int fillWidth(int parentSize, int fillSize) {
return width.fill(fillSize);
}
public int fillHeight(int parentSize, int fillSize) {
return height.fill(fillSize);
}
public int getWidth() {
return width.calculatedSize();
}
public int getHeight() {
return height.calculatedSize();
}
public void render(Rectangle parentBounds, Rectangle clipRect) {
Rectangle r = relativeBounds.movedBy(parentBounds.left, parentBounds.top);
Rectangle clip = r.intersect(clipRect);
if (r.overlaps(clip)) {
renderInBounds(r, clip);
}
}
protected void renderInBounds(Rectangle renderBounds, Rectangle clipRect) {
if (renderer != null) {
renderer.renderInBounds(renderBounds, clipRect);
}
}
void mouseEvent(MouseEvent event, int x, int y) {
onMouseEvent(event, x, y);
}
public boolean onMouseEvent(MouseEvent event, int x, int y) {
return false;
}
@Override
public String toString() {
return super.toString() + "(" + relativeBounds + ", " + width.calculatedSize() + "x" + height.calculatedSize() + ")";
}
}

View file

@ -0,0 +1,7 @@
package org.betterx.ui.layout.components;
import org.betterx.ui.layout.values.Rectangle;
public interface ComponentWithBounds {
Rectangle getRelativeBounds();
}

View file

@ -0,0 +1,112 @@
package org.betterx.ui.layout.components;
import org.betterx.ui.layout.components.render.*;
import org.betterx.ui.layout.components.input.*;
import org.betterx.ui.layout.values.*;
import java.util.LinkedList;
import java.util.List;
public class HorizontalStack<R extends ComponentRenderer> extends Component<R> {
protected final List<Component<?>> components = new LinkedList<>();
public HorizontalStack(DynamicSize width, DynamicSize height) {
this(width, height, null);
}
public HorizontalStack(DynamicSize width, DynamicSize height, R renderer) {
super(width, height, renderer);
}
@Override
public int updateContainerWidth(int containerWidth) {
int myWidth = width.calculateOrFill(containerWidth);
int fixedWidth = components.stream().map(c -> c.width.calculate(myWidth)).reduce(0, Integer::sum);
int freeWidth = Math.max(0, myWidth - fixedWidth);
fillWidth(myWidth, freeWidth);
for (Component<?> c : components) {
c.updateContainerWidth(c.width.calculatedSize());
}
return myWidth;
}
@Override
protected int updateContainerHeight(int containerHeight) {
int myHeight = height.calculateOrFill(containerHeight);
for (Component<?> c : components) {
c.updateContainerHeight(myHeight);
}
return myHeight;
}
@Override
void setRelativeBounds(int left, int top) {
super.setRelativeBounds(left, top);
int offset = 0;
for (Component<?> c : components) {
c.setRelativeBounds(offset, 0);
offset += c.relativeBounds.width;
}
}
@Override
public int getContentWidth() {
int fixedWidth = components.stream().map(c -> c.width.calculateFixed()).reduce(0, Integer::sum);
double percentage = components.stream().map(c -> c.width.calculateRelative()).reduce(0.0, Double::sum);
return (int) (fixedWidth / (1 - percentage));
}
@Override
public int getContentHeight() {
return components.stream().map(c -> c.height.calculateFixed()).reduce(0, Integer::max);
}
@Override
public int fillWidth(int parentSize, int fillSize) {
double totalFillWeight = components.stream().map(c -> c.width.fillWeight()).reduce(0.0, Double::sum);
return components.stream()
.map(c -> c.width.fill(fillSize, totalFillWeight))
.reduce(0, Integer::sum);
}
@Override
public int fillHeight(int parentSize, int fillSize) {
double totalFillHeight = components.stream().map(c -> c.height.fillWeight()).reduce(0.0, Double::sum);
return components.stream()
.map(c -> c.height.fill(fillSize, totalFillHeight))
.reduce(0, Integer::sum);
}
// @Override
// public int fillHeight(int parentSize, int fillSize) {
// return parentSize;
// }
@Override
protected void renderInBounds(Rectangle renderBounds, Rectangle clipRect) {
super.renderInBounds(renderBounds, clipRect);
for (Component<?> c : components) {
c.render(renderBounds, clipRect);
}
}
@Override
void mouseEvent(MouseEvent event, int x, int y) {
if (!onMouseEvent(event, x, y)) {
for (Component<?> c : components) {
c.mouseEvent(event, x - relativeBounds.left, y - relativeBounds.top);
}
}
}
public HorizontalStack<R> add(Component<?> c) {
this.components.add(c);
return this;
}
}

View file

@ -0,0 +1,43 @@
package org.betterx.ui.layout.components;
import org.betterx.ui.layout.components.input.MouseEvent;
import org.betterx.ui.layout.values.Rectangle;
public class Panel implements ComponentWithBounds {
protected Component<?> child;
public final Rectangle bounds;
public Panel(int width, int height) {
bounds = new Rectangle(50, 50, width, height);
}
public void setChild(Component<?> c) {
this.child = c;
}
public void mouseEvent(MouseEvent event, int x, int y) {
if (child != null) {
child.mouseEvent(event, x - bounds.left, y - bounds.top);
}
}
public void calculateLayout() {
if (child != null) {
child.updateContainerWidth(bounds.width);
child.updateContainerHeight(bounds.height);
child.setRelativeBounds(0, 0);
}
}
public void render() {
if (child != null) {
child.render(bounds, bounds);
}
}
@Override
public Rectangle getRelativeBounds() {
return bounds;
}
}

View file

@ -0,0 +1,142 @@
package org.betterx.ui.layout.components;
import org.betterx.ui.layout.components.input.MouseEvent;
import org.betterx.ui.layout.components.render.ComponentRenderer;
import org.betterx.ui.layout.components.render.ScrollerRenderer;
import org.betterx.ui.layout.values.DynamicSize;
import org.betterx.ui.layout.values.Rectangle;
public class VerticalScroll<R extends ComponentRenderer, RS extends ScrollerRenderer> extends Component<R> {
protected Component<?> child;
protected final RS scrollerRenderer;
protected int dist;
protected int scrollerY;
protected int scrollerHeight;
protected int travel;
protected int topOffset;
public VerticalScroll(DynamicSize width, DynamicSize height, RS scrollerRenderer) {
this(width, height, scrollerRenderer, null);
}
public VerticalScroll(DynamicSize width, DynamicSize height, RS scrollerRenderer, R renderer) {
super(width, height, renderer);
this.scrollerRenderer = scrollerRenderer;
}
public void setChild(Component<?> c) {
this.child = c;
}
@Override
protected int updateContainerWidth(int containerWidth) {
int myWidth = width.calculateOrFill(containerWidth);
if (child != null) {
child.updateContainerWidth(myWidth);
}
return myWidth;
}
@Override
protected int updateContainerHeight(int containerHeight) {
int myHeight = height.calculateOrFill(containerHeight);
if (child != null) {
child.updateContainerHeight(myHeight);
}
return myHeight;
}
@Override
public int getContentWidth() {
return child != null ? child.getContentWidth() : 0;
}
@Override
public int getContentHeight() {
return child != null ? child.getContentHeight() : 0;
}
@Override
void setRelativeBounds(int left, int top) {
super.setRelativeBounds(left, top);
if (child != null)
child.setRelativeBounds(0, 0);
updateScrollViewMetrics();
}
@Override
void mouseEvent(MouseEvent event, int x, int y) {
if (!onMouseEvent(event, x, y)) {
if (child != null) {
child.mouseEvent(event, x - relativeBounds.left, y - relativeBounds.top - scrollerOffset());
}
}
}
@Override
protected void renderInBounds(Rectangle renderBounds, Rectangle clipRect) {
super.renderInBounds(renderBounds, clipRect);
if (showScrollBar()) {
if (child != null) {
child.render(renderBounds.movedBy(0, scrollerOffset(), scrollerRenderer.scrollerWidth(), 0), clipRect);
}
scrollerRenderer.renderScrollBar(renderBounds, saveScrollerY(), scrollerHeight);
} else {
if (child != null) {
child.render(renderBounds, clipRect);
}
}
}
private boolean mouseDown = false;
private int mouseDownY = 0;
private int scrollerDownY = 0;
@Override
public boolean onMouseEvent(MouseEvent event, int x, int y) {
if (event == MouseEvent.DOWN) {
Rectangle scroller = scrollerRenderer.getScrollerBounds(relativeBounds);
Rectangle picker = scrollerRenderer.getPickerBounds(relativeBounds, saveScrollerY(), scrollerHeight);
if (picker.contains(x, y)) {
mouseDown = true;
mouseDownY = y;
scrollerDownY = saveScrollerY();
return true;
}
} else if (event == MouseEvent.UP) {
mouseDown = false;
} else if (event == MouseEvent.MOVE && mouseDown) {
int delta = y - mouseDownY;
scrollerY = scrollerDownY + delta;
return true;
}
return mouseDown;
}
protected void updateScrollViewMetrics() {
final int view = relativeBounds.height;
final int content = child.relativeBounds.height;
this.dist = content - view;
this.scrollerHeight = Math.max(scrollerRenderer.scrollerHeight(), (view * view) / content);
this.travel = view - this.scrollerHeight;
this.topOffset = 0;
this.scrollerY = 0;
}
protected int saveScrollerY() {
return Math.max(0, Math.min(travel, scrollerY));
}
protected int scrollerOffset() {
return -((int) (((float) saveScrollerY() / travel) * this.dist));
}
public boolean showScrollBar() {
return child.relativeBounds.height > relativeBounds.height;
}
}

View file

@ -0,0 +1,112 @@
package org.betterx.ui.layout.components;
import org.betterx.ui.layout.components.input.MouseEvent;
import org.betterx.ui.layout.components.render.ComponentRenderer;
import org.betterx.ui.layout.values.DynamicSize;
import org.betterx.ui.layout.values.Rectangle;
import java.util.LinkedList;
import java.util.List;
public class VerticalStack<R extends ComponentRenderer> extends Component<R> {
protected final List<Component<?>> components = new LinkedList<>();
public VerticalStack(DynamicSize width, DynamicSize height) {
this(width, height, null);
}
public VerticalStack(DynamicSize width, DynamicSize height, R renderer) {
super(width, height, renderer);
}
@Override
protected int updateContainerWidth(int containerWidth) {
int myWidth = width.calculateOrFill(containerWidth);
for (Component<?> c : components) {
c.updateContainerWidth(myWidth);
}
return myWidth;
}
@Override
public int updateContainerHeight(int containerHeight) {
int myHeight = height.calculateOrFill(containerHeight);
int fixedHeight = components.stream().map(c -> c.height.calculate(myHeight)).reduce(0, Integer::sum);
int freeHeight = Math.max(0, myHeight - fixedHeight);
fillHeight(myHeight, freeHeight);
for (Component<?> c : components) {
c.updateContainerHeight(c.height.calculatedSize());
}
return myHeight;
}
@Override
void setRelativeBounds(int left, int top) {
super.setRelativeBounds(left, top);
int offset = 0;
for (Component<?> c : components) {
c.setRelativeBounds(0, offset);
offset += c.relativeBounds.height;
}
}
@Override
public int getContentWidth() {
return components.stream().map(c -> c.width.calculateFixed()).reduce(0, Integer::max);
}
@Override
public int getContentHeight() {
int fixedHeight = components.stream().map(c -> c.height.calculateFixed()).reduce(0, Integer::sum);
double percentage = components.stream().map(c -> c.height.calculateRelative()).reduce(0.0, Double::sum);
return (int) (fixedHeight / (1 - percentage));
}
// @Override
// public int fillWidth(int parentSize, int fillSize) {
// return parentSize;
// }
@Override
public int fillWidth(int parentSize, int fillSize) {
double totalFillWeight = components.stream().map(c -> c.width.fillWeight()).reduce(0.0, Double::sum);
return components.stream()
.map(c -> c.width.fill(fillSize, totalFillWeight))
.reduce(0, Integer::sum);
}
@Override
public int fillHeight(int parentSize, int fillSize) {
double totalFillHeight = components.stream().map(c -> c.height.fillWeight()).reduce(0.0, Double::sum);
return components.stream()
.map(c -> c.height.fill(fillSize, totalFillHeight))
.reduce(0, Integer::sum);
}
@Override
protected void renderInBounds(Rectangle renderBounds, Rectangle clipRect) {
super.renderInBounds(renderBounds, clipRect);
for (Component<?> c : components) {
c.render(renderBounds, clipRect);
}
}
@Override
void mouseEvent(MouseEvent event, int x, int y) {
if (!onMouseEvent(event, x, y)) {
for (Component<?> c : components) {
c.mouseEvent(event, x - relativeBounds.left, y - relativeBounds.top);
}
}
}
public VerticalStack<R> add(Component<?> c) {
this.components.add(c);
return this;
}
}

View file

@ -0,0 +1,5 @@
package org.betterx.ui.layout.components.input;
public enum MouseEvent {
DOWN, UP, MOVE
}

View file

@ -0,0 +1,7 @@
package org.betterx.ui.layout.components.render;
import org.betterx.ui.layout.values.Rectangle;
public interface ComponentRenderer {
void renderInBounds(Rectangle bounds, Rectangle clipRect);
}

View file

@ -0,0 +1,33 @@
package org.betterx.ui.layout.components.render;
import org.betterx.ui.layout.values.Rectangle;
public interface ScrollerRenderer {
default int scrollerHeight() {
return 16;
}
default int scrollerWidth() {
return 16;
}
default Rectangle getScrollerBounds(Rectangle renderBounds) {
return new Rectangle(
renderBounds.right() - this.scrollerWidth(),
renderBounds.top,
this.scrollerWidth(),
renderBounds.height
);
}
default Rectangle getPickerBounds(Rectangle renderBounds, int pickerOffset, int pickerSize) {
return new Rectangle(
renderBounds.left,
renderBounds.top + pickerOffset,
renderBounds.width,
pickerSize
);
}
void renderScrollBar(Rectangle renderBounds, int pickerOffset, int pickerSize);
}

View file

@ -0,0 +1,102 @@
package org.betterx.ui.layout.values;
public class DynamicSize {
private SizeType sizeType;
private int calculatedSize;
public DynamicSize(SizeType sizeType) {
this.sizeType = sizeType;
this.calculatedSize = 0;
}
public static DynamicSize fixed(int size) {
return new DynamicSize(new SizeType.Fixed(size));
}
public static DynamicSize relative(double percentage) {
return new DynamicSize(new SizeType.Relative(percentage));
}
public static DynamicSize fill() {
return new DynamicSize(SizeType.FILL);
}
public static DynamicSize fit() {
return new DynamicSize(SizeType.FIT_CONTENT);
}
public int calculatedSize() {
return calculatedSize;
}
public int setCalculatedSize(int value) {
calculatedSize = value;
return value;
}
public DynamicSize attachComponent(SizeType.FitContent.ContentSizeSupplier c) {
if (sizeType instanceof SizeType.FitContent fit && fit.contentSize() == null) {
sizeType = fit.copyForSupplier(c);
}
return this;
}
public int calculateFixed() {
return calculate(0);
}
public double calculateRelative() {
if (sizeType instanceof SizeType.Relative rel) {
return rel.percentage();
}
return 0;
}
public int calculate(int parentSize) {
calculatedSize = 0;
if (sizeType instanceof SizeType.Fixed fixed) {
calculatedSize = fixed.size();
} else if (sizeType instanceof SizeType.FitContent fit) {
calculatedSize = fit.contentSize().get();
} else if (sizeType instanceof SizeType.Relative rel) {
calculatedSize = (int) (parentSize * rel.percentage());
}
return calculatedSize;
}
public int calculateOrFill(int parentSize) {
calculatedSize = calculate(parentSize);
if (sizeType instanceof SizeType.Fill) {
calculatedSize = parentSize;
}
return calculatedSize;
}
public double fillWeight() {
if (sizeType instanceof SizeType.Fill fill) {
return 1;
}
return 0;
}
public int fill(int fillSize) {
return fill(fillSize, fillWeight());
}
public int fill(int fillSize, double totalFillWeight) {
if (sizeType instanceof SizeType.Fill) {
calculatedSize = (int) Math.round(fillSize * (fillWeight() / totalFillWeight));
}
return calculatedSize;
}
@Override
public String toString() {
return "DynamicSize{" +
"sizeType=" + sizeType.getClass().getSimpleName() +
", calculatedSize=" + calculatedSize +
'}';
}
}

View file

@ -0,0 +1,63 @@
package org.betterx.ui.layout.values;
public class Rectangle {
public static final Rectangle ZERO = new Rectangle(0, 0, 0, 0);
public final int left;
public final int top;
public final int width;
public final int height;
public Rectangle(int left, int top, int width, int height) {
this.left = left;
this.top = top;
this.width = width;
this.height = height;
}
public int right() {
return left + width;
}
public int bottom() {
return top + height;
}
public Rectangle movedBy(int left, int top) {
return new Rectangle(this.left + left, this.top + top, this.width, this.height);
}
public Rectangle movedBy(int left, int top, int deltaWidth, int deltaHeight) {
return new Rectangle(this.left + left, this.top + top, this.width + deltaWidth, this.height + deltaHeight);
}
public boolean overlaps(Rectangle r) {
return this.left < r.right() && this.right() > r.left &&
this.top < r.bottom() && this.bottom() > r.top;
}
public boolean contains(int x, int y) {
return x >= left && x <= right() && y >= top && y <= bottom();
}
public Rectangle intersect(Rectangle r) {
if (!overlaps(r)) return ZERO;
int left = Math.max(this.left, r.left);
int top = Math.max(this.top, r.top);
int right = Math.min(this.right(), r.right());
int bottom = Math.min(this.bottom(), r.bottom());
return new Rectangle(left, top, right - left, bottom - top);
}
@Override
public String toString() {
return "rectangle{" +
left +
"x" + top +
", " + right() +
"x" + bottom() +
" [" + width +
"x" + height +
"]}";
}
}

View file

@ -0,0 +1,38 @@
package org.betterx.ui.layout.values;
public interface SizeType {
FitContent FIT_CONTENT = new FitContent();
Fill FILL = new Fill();
record Fill() implements SizeType {
}
record FitContent(ContentSizeSupplier contentSize) implements SizeType {
@FunctionalInterface
public interface ContentSizeSupplier {
int get();
}
public FitContent() {
this(null);
}
public FitContent copyForSupplier(ContentSizeSupplier component) {
return new FitContent(component);
}
}
record Fixed(int size) implements SizeType {
@Override
public String toString() {
return getClass().getSimpleName() + "(" + size + ")";
}
}
record Relative(double percentage) implements SizeType {
@Override
public String toString() {
return getClass().getSimpleName() + "(" + percentage + ")";
}
}
}