Skip to content

Commit d303958

Browse files
committed
Draw selection rectangles
1 parent f790360 commit d303958

File tree

6 files changed

+149
-58
lines changed

6 files changed

+149
-58
lines changed

examples/src/main/java/org/jsignal/examples/todo/CardPainter.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@ public non-sealed class CardPainter extends CardPainterPropHelper implements Pai
1717
@Prop
1818
Supplier<Integer> backgroundColor = Constant.of(EzColors.WHITE);
1919
@Prop
20-
Supplier<Float> radius = Constant.of(4f);
20+
Supplier<Integer> shadowColor = Constant.of(EzColors.GRAY_500);
21+
@Prop
22+
Supplier<Float> radius = Constant.of(8f);
2123

2224
@Override
2325
public void paint(Canvas canvas, Layout layout) {
2426
try (var paint = new Paint()) {
25-
paint.setImageFilter(ImageFilter.makeDropShadow(0f, 4f, 4f, 4f, EzColors.GRAY_500));
27+
paint.setImageFilter(ImageFilter.makeDropShadow(0f, 4f, 4f, 4f, shadowColor.get()));
2628
paint.setColor(backgroundColor.get());
2729
canvas.drawRRect(layout.getBoundingRect().withRadii(radius.get()), paint);
2830
}

examples/src/main/java/org/jsignal/examples/todo/TodoApp.java

+16-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.jsignal.examples.todo;
22

3+
import io.github.humbleui.skija.FontStyle;
34
import org.jsignal.rx.Signal;
45
import org.jsignal.std.*;
56
import org.jsignal.std.ez.EzColors;
@@ -117,20 +118,29 @@ protected Element render() {
117118
enterAnim.start();
118119

119120
return Node.builder()
120-
.paint(CardPainter.builder()
121-
.radius(16f)
122-
.build()
123-
)
121+
.paint(new CardPainter())
124122
.transform(layout -> MathUtil.scaleCenter(enterAnim.get(), layout.getWidth(), layout.getHeight()))
125123
.layoutBuilder(lb -> lb
126124
.maxWidth(percent(100f))
127125
.padding(insets(16f).pixels())
128126
.row()
129127
.gap(16f)
130-
.justify(LayoutConfig.JustifyContent.BETWEEN)
128+
.justify(LayoutConfig.JustifyContent.START)
131129
.alignItems(LayoutConfig.Align.CENTER)
132130
)
133131
.children(compose(
132+
Node.builder()
133+
.layoutBuilder(lb -> lb.alignSelf(LayoutConfig.Align.START))
134+
.children(
135+
Para.builder()
136+
.string(idx.get().toString() + ")")
137+
.styleBuilder(sb -> sb.textStyleBuilder(tsb -> tsb
138+
.fontSize(20f)
139+
.fontStyle(FontStyle.BOLD)
140+
))
141+
.build()
142+
)
143+
.build(),
134144
Node.builder()
135145
.layoutBuilder(lb -> lb.shrink(1f))
136146
.children(
@@ -142,6 +152,7 @@ protected Element render() {
142152
.build()
143153
)
144154
.build(),
155+
Node.builder().layoutBuilder(lb -> lb.grow(1f)).build(),
145156
Button.builder()
146157
.color(EzColors.RED_600)
147158
.action(() -> todos.modify(list -> list.remove((int) idx.get())))

std/src/main/java/org/jsignal/std/Para.java

+7-4
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,14 @@ public static class Transitive {
6060
Supplier<ParaStyle> style = ParaStyle.context.use();
6161

6262
private final Signal<Float> height = Signal.empty();
63+
private final Ref<Node> ref = new Ref<>();
6364

6465
public Para() {}
6566

67+
public Node getRef() {
68+
return ref.get();
69+
}
70+
6671
@Override
6772
protected void onBuild(Transitive transitive) {
6873
if (transitive.styleBuilder != null) {
@@ -80,11 +85,9 @@ protected void onBuild(Transitive transitive) {
8085

8186
@Override
8287
public Element render() {
83-
Ref<Node> nodeRef = new Ref<>();
84-
onResolve(() -> Effect.create(onDefer(para, () -> nodeRef.get().getLayoutConfig().markDirty())));
85-
88+
onResolve(() -> Effect.create(onDefer(para, () -> ref.get().getLayoutConfig().markDirty())));
8689
return Node.builder()
87-
.ref(nodeRef)
90+
.ref(ref)
8891
.layout(config ->
8992
config.setMeasure((width, widthMode, height, heightMode) -> {
9093
var layoutWidth = widthMode == LayoutConfig.MeasureMode.UNDEFINED ? Float.POSITIVE_INFINITY : width;

std/src/main/java/org/jsignal/std/TextInput.java

+79-11
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,24 @@
44
import io.github.humbleui.jwm.KeyModifier;
55
import io.github.humbleui.jwm.MouseCursor;
66
import io.github.humbleui.skija.Canvas;
7+
import io.github.humbleui.skija.Matrix33;
78
import io.github.humbleui.skija.Paint;
89
import io.github.humbleui.skija.paragraph.RectHeightMode;
910
import io.github.humbleui.skija.paragraph.RectWidthMode;
1011
import org.jsignal.prop.GeneratePropComponent;
1112
import org.jsignal.prop.Prop;
1213
import org.jsignal.prop.TransitiveProps;
1314
import org.jsignal.rx.Constant;
15+
import org.jsignal.rx.Effect;
1416
import org.jsignal.rx.Ref;
1517
import org.jsignal.rx.Signal;
1618
import org.jsignal.std.ez.EzColors;
1719
import org.jsignal.std.ez.EzLayout;
1820
import org.jsignal.ui.Element;
21+
import org.jsignal.ui.MathUtil;
1922
import org.jsignal.ui.Node;
2023
import org.jsignal.ui.UiWindow;
24+
import org.jsignal.ui.event.MouseEvent;
2125
import org.jsignal.ui.layout.Layout;
2226
import org.jsignal.ui.layout.Layouter;
2327

@@ -52,7 +56,10 @@ public static class Transitive {
5256
private final Signal<Boolean> isFocused = Signal.create(false);
5357

5458
private Para para;
59+
private final Ref<Node> ref = new Ref<>();
5560
private final Signal<Optional<Integer>> cursorPosition = Signal.create(Optional.empty());
61+
private final Signal<Boolean> mouseDown = Signal.create(false);
62+
private final Signal<Optional<SelectionRange>> selectionRange = Signal.create(Optional.empty());
5663

5764
@Override
5865
protected void onBuild(Transitive transitive) {
@@ -61,10 +68,21 @@ protected void onBuild(Transitive transitive) {
6168

6269
@Override
6370
public Element render() {
64-
var ref = new Ref<Node>();
65-
6671
var window = UiWindow.context.use().getWindow().get();
6772

73+
Effect.create(() -> {
74+
if (mouseDown.get()) {
75+
Effect.create(() -> {
76+
var pos = MathUtil.apply(
77+
MathUtil.inverse(para.getRef().getFullTransform()),
78+
UiWindow.context.use().getMousePosition()
79+
);
80+
var gPos = para.getParagraph().getGlyphPositionAtCoordinate(pos.getX(), pos.getY());
81+
selectionRange.accept(Optional.of(new SelectionRange(selectionRange.get().get().start, gPos.getPosition())));
82+
});
83+
}
84+
});
85+
6886
return Node.builder()
6987
.ref(ref)
7088
.listen(List.of(
@@ -79,16 +97,18 @@ public Element render() {
7997
onBlur(event -> {
8098
isFocused.accept(false);
8199
cursorPosition.accept(Optional.empty());
100+
selectionRange.accept(Optional.empty());
101+
}),
102+
onMouseDown(event -> {
103+
var pos = mouseEventToPosition(event, ref.get());
104+
cursorPosition.accept(Optional.empty());
105+
pos.ifPresent(p -> selectionRange.accept(Optional.of(new SelectionRange(p, p))));
106+
mouseDown.accept(true);
82107
}),
108+
onMouseUp(event -> mouseDown.accept(false)),
83109
onMouseClick(event -> {
84-
if (content.get().isEmpty()) {
85-
cursorPosition.accept(Optional.of(0));
86-
} else {
87-
var pos = event.getPoint();
88-
var content = ref.get().getLayout().getContentRect();
89-
var x = pos.getX() - content.getLeft();
90-
var y = pos.getY() - content.getTop();
91-
cursorPosition.accept(Optional.of(para.getParagraph().getGlyphPositionAtCoordinate(x, y).getPosition()));
110+
if (selectionRange.get().isEmpty() || selectionRange.get().get().start == selectionRange.get().get().end) {
111+
cursorPosition.accept(mouseEventToPosition(event, ref.get()));
92112
}
93113
}),
94114
onKeyDown(event -> {
@@ -143,12 +163,26 @@ public Element render() {
143163
layout.getPaddingRect().withRadii(radius), paint
144164
);
145165
}
166+
167+
paintSelection(canvas, layout);
146168
})
147169
.paintAfter(this::paintCursor)
148170
.children(children.apply(para))
149171
.build();
150172
}
151173

174+
private Optional<Integer> mouseEventToPosition(MouseEvent e, Node node) {
175+
if (content.get().isEmpty()) {
176+
return Optional.of(0);
177+
} else {
178+
var pos = e.getPoint();
179+
var content = node.getLayout().getContentRect();
180+
var x = pos.getX() - content.getLeft();
181+
var y = pos.getY() - content.getTop();
182+
return Optional.of(para.getParagraph().getGlyphPositionAtCoordinate(x, y).getPosition());
183+
}
184+
}
185+
152186
private void paintCursor(Canvas canvas, Layout layout) {
153187
cursorPosition.get().ifPresent(pos -> {
154188
try (var paint = new Paint()) {
@@ -163,7 +197,7 @@ private void paintCursor(Canvas canvas, Layout layout) {
163197
return;
164198
}
165199

166-
var p = ignore(() -> para.getParagraph());
200+
var p = para.getParagraph();
167201
var lineMetrics = p.getLineMetrics();
168202
var lastLineMetrics = lineMetrics[lineMetrics.length - 1];
169203
int endPos = (int) lastLineMetrics.getEndIndex();
@@ -177,6 +211,40 @@ private void paintCursor(Canvas canvas, Layout layout) {
177211
});
178212
}
179213

214+
private void paintSelection(Canvas canvas, Layout layout) {
215+
selectionRange.get().ifPresent(range -> {
216+
if (range.start == range.end) {
217+
return;
218+
}
219+
220+
var p = para.getParagraph();
221+
var rects = p.getRectsForRange(range.getMin(), range.getMax(), RectHeightMode.MAX, RectWidthMode.MAX);
222+
var contentRect = layout.getContentRect();
223+
int count = canvas.save();
224+
try {
225+
canvas.concat(Matrix33.makeTranslate(contentRect.getLeft(), contentRect.getTop()));
226+
try (var paint = new Paint()) {
227+
paint.setColor(EzColors.BLUE_400);
228+
for (var rect : rects) {
229+
canvas.drawRect(rect.getRect(), paint);
230+
}
231+
}
232+
} finally {
233+
canvas.restoreToCount(count);
234+
}
235+
});
236+
}
237+
238+
private record SelectionRange(int start, int end) {
239+
public int getMin() {
240+
return Math.min(start, end);
241+
}
242+
243+
public int getMax() {
244+
return Math.max(start, end);
245+
}
246+
}
247+
180248
public static String keyToInputString(Key key, boolean lower) {
181249
if (lower) {
182250
return switch (key) {

ui/src/main/java/org/jsignal/ui/Node.java

+34-36
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,7 @@
88
import org.jsignal.prop.GeneratePropHelper;
99
import org.jsignal.prop.Prop;
1010
import org.jsignal.prop.TransitiveProps;
11-
import org.jsignal.rx.Cleanups;
12-
import org.jsignal.rx.Constant;
13-
import org.jsignal.rx.Context;
14-
import org.jsignal.rx.Effect;
15-
import org.jsignal.rx.Ref;
16-
import org.jsignal.rx.RxUtil;
17-
import org.jsignal.rx.SideEffect;
11+
import org.jsignal.rx.*;
1812
import org.jsignal.ui.event.Event;
1913
import org.jsignal.ui.event.EventListener;
2014
import org.jsignal.ui.event.EventType;
@@ -27,18 +21,7 @@
2721
import org.jsignal.ui.paint.UpgradingPaintCacheStrategy;
2822
import org.lwjgl.util.yoga.Yoga;
2923

30-
import java.util.ArrayList;
31-
import java.util.Arrays;
32-
import java.util.Collection;
33-
import java.util.Collections;
34-
import java.util.HashMap;
35-
import java.util.HashSet;
36-
import java.util.LinkedHashSet;
37-
import java.util.List;
38-
import java.util.Map;
39-
import java.util.Optional;
40-
import java.util.Set;
41-
import java.util.WeakHashMap;
24+
import java.util.*;
4225
import java.util.function.Consumer;
4326
import java.util.function.Function;
4427
import java.util.function.Supplier;
@@ -53,25 +36,40 @@ public non-sealed class Node extends NodePropHelper implements Nodes {
5336

5437
@TransitiveProps
5538
public static class Transitive {
56-
@Prop Collection<Object> tags = Collections.emptyList();
57-
@Prop Collection<EventListener<?>> listen = Collections.emptyList();
58-
@Prop Element children = Nodes.empty();
59-
@Prop Consumer<Node> ref;
60-
@Prop Function<CompositeLayouter.Builder, CompositeLayouter.Builder> layoutBuilder;
61-
}
62-
63-
@Prop Object id;
64-
65-
@Prop UiWindow window = UiWindow.context.use();
66-
67-
@Prop @Nullable Layouter layout;
68-
@Prop Transformer transform = n -> Matrix33.IDENTITY;
69-
@Prop @Nullable Painter paint;
70-
@Prop @Nullable Painter paintAfter;
71-
@Prop HitTester hitTest = HitTester::boundsTest;
39+
@Prop
40+
Collection<Object> tags = Collections.emptyList();
41+
@Prop
42+
Collection<EventListener<?>> listen = Collections.emptyList();
43+
@Prop
44+
Element children = Nodes.empty();
45+
@Prop
46+
Consumer<Node> ref;
47+
@Prop
48+
Function<CompositeLayouter.Builder, CompositeLayouter.Builder> layoutBuilder;
49+
}
50+
51+
@Prop
52+
Object id;
53+
54+
@Prop
55+
UiWindow window = UiWindow.context.use();
56+
57+
@Prop
58+
@Nullable
59+
Layouter layout;
60+
@Prop
61+
Transformer transform = n -> Matrix33.IDENTITY;
62+
@Prop
63+
@Nullable
64+
Painter paint;
65+
@Prop
66+
@Nullable
67+
Painter paintAfter;
68+
@Prop
69+
HitTester hitTest = HitTester::boundsTest;
7270

7371
private @Nullable Node parent;
74-
72+
7573
private final Set<Object> tags;
7674

7775
private final long yoga;

ui/src/main/java/org/jsignal/ui/layout/CompositeLayouter.java

+9
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,15 @@ public Builder center() {
5050
return this;
5151
}
5252

53+
public Builder alignSelf(LayoutConfig.Align align) {
54+
return alignSelf(Constant.of(align));
55+
}
56+
57+
public Builder alignSelf(Supplier<LayoutConfig.Align> align) {
58+
operations.add(config -> config.setAlignSelf(align.get()));
59+
return this;
60+
}
61+
5362
public Builder justify(LayoutConfig.JustifyContent justify) {
5463
return this.justify(Constant.of(justify));
5564
}

0 commit comments

Comments
 (0)