Skip to content

Commit 86cc4cd

Browse files
Migrate to InputMapTemplate & InputMap behavior approach
1 parent 6d812f4 commit 86cc4cd

1 file changed

Lines changed: 119 additions & 144 deletions

File tree

richtextfx/src/main/java/org/fxmisc/richtext/StyledTextAreaBehavior.java

Lines changed: 119 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@
22

33
import static javafx.scene.input.KeyCode.*;
44
import static javafx.scene.input.KeyCombination.*;
5-
import static javafx.scene.input.KeyEvent.*;
65
import static org.fxmisc.richtext.TwoDimensional.Bias.*;
7-
import static org.fxmisc.wellbehaved.event.EventPattern.*;
6+
import static org.fxmisc.wellbehaved.event.experimental.EventPattern.*;
7+
import static org.fxmisc.wellbehaved.event.experimental.template.InputMapTemplate.consume;
8+
import static org.fxmisc.wellbehaved.event.experimental.template.InputMapTemplate.sequence;
9+
import static org.fxmisc.wellbehaved.event.experimental.template.InputMapTemplate.when;
810
import static org.reactfx.EventStreams.*;
911

1012
import java.util.Optional;
1113
import java.util.function.Predicate;
1214

13-
import javafx.event.EventHandler;
15+
import javafx.event.Event;
1416
import javafx.geometry.Bounds;
1517
import javafx.geometry.Point2D;
1618
import javafx.scene.control.IndexRange;
@@ -21,8 +23,8 @@
2123
import org.fxmisc.richtext.NavigationActions.SelectionPolicy;
2224
import org.fxmisc.richtext.TwoDimensional.Position;
2325
import org.fxmisc.richtext.ParagraphBox.CaretOffsetX;
24-
import org.fxmisc.wellbehaved.event.EventHandlerHelper;
25-
import org.fxmisc.wellbehaved.event.EventHandlerTemplate;
26+
import org.fxmisc.wellbehaved.event.experimental.EventPattern;
27+
import org.fxmisc.wellbehaved.event.experimental.template.InputMapTemplate;
2628
import org.fxmisc.wellbehaved.skin.Behavior;
2729
import org.reactfx.EventStream;
2830
import org.reactfx.Subscription;
@@ -42,100 +44,112 @@ class StyledTextAreaBehavior implements Behavior {
4244
isWindows = os.startsWith("Windows");
4345
}
4446

45-
private static final EventHandlerTemplate<StyledTextAreaBehavior, ? super KeyEvent> KEY_PRESSED_TEMPLATE;
46-
private static final EventHandlerTemplate<StyledTextAreaBehavior, ? super KeyEvent> KEY_TYPED_TEMPLATE;
47-
private static final EventHandlerTemplate<StyledTextAreaBehavior, ? super MouseEvent> MOUSE_PRESSED_TEMPLATE;
48-
private static final EventHandlerTemplate<StyledTextAreaBehavior, ? super MouseEvent> MOUSE_DRAGGED_TEMPLATE;
49-
private static final EventHandlerTemplate<StyledTextAreaBehavior, ? super MouseEvent> DRAG_DETECTED_TEMPLATE;
50-
private static final EventHandlerTemplate<StyledTextAreaBehavior, ? super MouseEvent> MOUSE_RELEASED_TEMPLATE;
47+
private static final InputMapTemplate<StyledTextAreaBehavior, ? super Event> EVENT_TEMPLATE;
5148

5249
static {
5350
SelectionPolicy selPolicy = isMac
5451
? SelectionPolicy.EXTEND
5552
: SelectionPolicy.ADJUST;
5653

57-
EventHandlerTemplate<StyledTextAreaBehavior, KeyEvent> edits = EventHandlerTemplate
54+
InputMapTemplate<StyledTextAreaBehavior, KeyEvent> editsBase = sequence(
5855
// deletion
59-
.on(keyPressed(DELETE)) .act(StyledTextAreaBehavior::deleteForward)
60-
.on(keyPressed(BACK_SPACE)) .act(StyledTextAreaBehavior::deleteBackward)
61-
.on(keyPressed(DELETE, SHORTCUT_DOWN)).act(StyledTextAreaBehavior::deleteNextWord)
62-
.on(keyPressed(BACK_SPACE, SHORTCUT_DOWN)).act(StyledTextAreaBehavior::deletePrevWord)
56+
consume(keyPressed(DELETE), StyledTextAreaBehavior::deleteForward),
57+
consume(keyPressed(BACK_SPACE), StyledTextAreaBehavior::deleteBackward),
58+
consume(keyPressed(DELETE, SHORTCUT_DOWN), StyledTextAreaBehavior::deleteNextWord),
59+
consume(keyPressed(BACK_SPACE, SHORTCUT_DOWN), StyledTextAreaBehavior::deletePrevWord),
6360
// cut
64-
.on(keyPressed(CUT)) .act((b, e) -> b.view.cut())
65-
.on(keyPressed(X, SHORTCUT_DOWN)) .act((b, e) -> b.view.cut())
66-
.on(keyPressed(DELETE, SHIFT_DOWN)).act((b, e) -> b.view.cut())
61+
consume(
62+
anyOf(keyPressed(CUT), keyPressed(X, SHORTCUT_DOWN), keyPressed(DELETE, SHIFT_DOWN)),
63+
(b, e) -> b.view.cut()),
6764
// paste
68-
.on(keyPressed(PASTE)) .act((b, e) -> b.view.paste())
69-
.on(keyPressed(V, SHORTCUT_DOWN)) .act((b, e) -> b.view.paste())
70-
.on(keyPressed(INSERT, SHIFT_DOWN)).act((b, e) -> b.view.paste())
65+
consume(
66+
anyOf(keyPressed(PASTE), keyPressed(V, SHORTCUT_DOWN), keyPressed(INSERT, SHIFT_DOWN)),
67+
(b, e) -> b.view.paste()),
7168
// tab & newline
72-
.on(keyPressed(ENTER)).act((b, e) -> b.model.replaceSelection("\n"))
73-
.on(keyPressed(TAB)) .act((b, e) -> b.model.replaceSelection("\t"))
74-
// undo/redo,
75-
.on(keyPressed(Z, SHORTCUT_DOWN)) .act((b, e) -> b.model.undo())
76-
.on(keyPressed(Y, SHORTCUT_DOWN)) .act((b, e) -> b.model.redo())
77-
.on(keyPressed(Z, SHORTCUT_DOWN, SHIFT_DOWN)).act((b, e) -> b.model.redo())
78-
79-
.create()
80-
.onlyWhen(b -> b.view.isEditable());
81-
82-
EventHandlerTemplate<StyledTextAreaBehavior, KeyEvent> verticalNavigation = EventHandlerTemplate
83-
.<StyledTextAreaBehavior, KeyEvent, KeyEvent>
69+
consume(keyPressed(ENTER), (b, e) -> b.model.replaceSelection("\n")),
70+
consume(keyPressed(TAB), (b, e) -> b.model.replaceSelection("\t")),
71+
// undo/redo
72+
consume(keyPressed(Z, SHORTCUT_DOWN), (b, e) -> b.model.undo()),
73+
consume(
74+
anyOf(keyPressed(Y, SHORTCUT_DOWN), keyPressed(Z, SHORTCUT_DOWN, SHIFT_DOWN)),
75+
(b, e) -> b.model.redo())
76+
);
77+
InputMapTemplate<StyledTextAreaBehavior, KeyEvent> edits = when(b -> b.view.isEditable(), editsBase);
78+
79+
InputMapTemplate<StyledTextAreaBehavior, KeyEvent> verticalNavigation = sequence(
8480
// vertical caret movement
85-
on(keyPressed(UP)) .act((b, e) -> b.prevLine(SelectionPolicy.CLEAR))
86-
.on(keyPressed(KP_UP)) .act((b, e) -> b.prevLine(SelectionPolicy.CLEAR))
87-
.on(keyPressed(DOWN)) .act((b, e) -> b.nextLine(SelectionPolicy.CLEAR))
88-
.on(keyPressed(KP_DOWN)) .act((b, e) -> b.nextLine(SelectionPolicy.CLEAR))
89-
.on(keyPressed(PAGE_UP)) .act((b, e) -> b.prevPage(SelectionPolicy.CLEAR))
90-
.on(keyPressed(PAGE_DOWN)).act((b, e) -> b.nextPage(SelectionPolicy.CLEAR))
81+
consume(
82+
anyOf(keyPressed(UP), keyPressed(KP_UP)),
83+
(b, e) -> b.prevLine(SelectionPolicy.CLEAR)),
84+
consume(
85+
anyOf(keyPressed(DOWN), keyPressed(KP_DOWN)),
86+
(b, e) -> b.nextLine(SelectionPolicy.CLEAR)),
87+
consume(keyPressed(PAGE_UP), (b, e) -> b.prevPage(SelectionPolicy.CLEAR)),
88+
consume(keyPressed(PAGE_DOWN), (b, e) -> b.nextPage(SelectionPolicy.CLEAR)),
9189
// vertical selection
92-
.on(keyPressed(UP, SHIFT_DOWN)).act((b, e) -> b.prevLine(SelectionPolicy.ADJUST))
93-
.on(keyPressed(KP_UP, SHIFT_DOWN)).act((b, e) -> b.prevLine(SelectionPolicy.ADJUST))
94-
.on(keyPressed(DOWN, SHIFT_DOWN)).act((b, e) -> b.nextLine(SelectionPolicy.ADJUST))
95-
.on(keyPressed(KP_DOWN, SHIFT_DOWN)).act((b, e) -> b.nextLine(SelectionPolicy.ADJUST))
96-
.on(keyPressed(PAGE_UP, SHIFT_DOWN)).act((b, e) -> b.prevPage(SelectionPolicy.ADJUST))
97-
.on(keyPressed(PAGE_DOWN, SHIFT_DOWN)).act((b, e) -> b.nextPage(SelectionPolicy.ADJUST))
98-
99-
.create();
100-
101-
EventHandlerTemplate<StyledTextAreaBehavior, KeyEvent> otherNavigation = EventHandlerTemplate
90+
consume(
91+
anyOf(keyPressed(UP, SHIFT_DOWN), keyPressed(KP_UP, SHIFT_DOWN)),
92+
(b, e) -> b.prevLine(SelectionPolicy.ADJUST)),
93+
consume(
94+
anyOf(keyPressed(DOWN, SHIFT_DOWN), keyPressed(KP_DOWN, SHIFT_DOWN)),
95+
(b, e) -> b.nextLine(SelectionPolicy.ADJUST)),
96+
consume(keyPressed(PAGE_UP, SHIFT_DOWN), (b, e) -> b.prevPage(SelectionPolicy.ADJUST)),
97+
consume(keyPressed(PAGE_DOWN, SHIFT_DOWN), (b, e) -> b.nextPage(SelectionPolicy.ADJUST))
98+
);
99+
100+
InputMapTemplate<StyledTextAreaBehavior, KeyEvent> otherNavigation = sequence(
102101
// caret movement
103-
.on(keyPressed(RIGHT)) .act(StyledTextAreaBehavior::right)
104-
.on(keyPressed(KP_RIGHT)).act(StyledTextAreaBehavior::right)
105-
.on(keyPressed(LEFT)) .act(StyledTextAreaBehavior::left)
106-
.on(keyPressed(KP_LEFT)) .act(StyledTextAreaBehavior::left)
107-
.on(keyPressed(HOME)) .act((b, e) -> b.model.lineStart(SelectionPolicy.CLEAR))
108-
.on(keyPressed(END)) .act((b, e) -> b.model.lineEnd(SelectionPolicy.CLEAR))
109-
.on(keyPressed(RIGHT, SHORTCUT_DOWN)).act((b, e) -> b.model.wordBreaksForwards(2, SelectionPolicy.CLEAR))
110-
.on(keyPressed(KP_RIGHT, SHORTCUT_DOWN)).act((b, e) -> b.model.wordBreaksForwards(2, SelectionPolicy.CLEAR))
111-
.on(keyPressed(LEFT, SHORTCUT_DOWN)).act((b, e) -> b.model.wordBreaksBackwards(2, SelectionPolicy.CLEAR))
112-
.on(keyPressed(KP_LEFT, SHORTCUT_DOWN)).act((b, e) -> b.model.wordBreaksBackwards(2, SelectionPolicy.CLEAR))
113-
.on(keyPressed(HOME, SHORTCUT_DOWN)).act((b, e) -> b.model.start(SelectionPolicy.CLEAR))
114-
.on(keyPressed(END, SHORTCUT_DOWN)).act((b, e) -> b.model.end(SelectionPolicy.CLEAR))
102+
consume(anyOf(keyPressed(RIGHT), keyPressed(KP_RIGHT)), StyledTextAreaBehavior::right),
103+
consume(anyOf(keyPressed(LEFT), keyPressed(KP_LEFT)), StyledTextAreaBehavior::left),
104+
consume(keyPressed(HOME), (b, e) -> b.model.lineStart(SelectionPolicy.CLEAR)),
105+
consume(keyPressed(END), (b, e) -> b.model.lineEnd(SelectionPolicy.CLEAR)),
106+
consume(
107+
anyOf(
108+
keyPressed(RIGHT, SHORTCUT_DOWN),
109+
keyPressed(KP_RIGHT, SHORTCUT_DOWN)
110+
), (b, e) -> b.model.wordBreaksForwards(2, SelectionPolicy.CLEAR)),
111+
consume(
112+
anyOf(
113+
keyPressed(LEFT, SHORTCUT_DOWN),
114+
keyPressed(KP_LEFT, SHORTCUT_DOWN)
115+
), (b, e) -> b.model.wordBreaksBackwards(2, SelectionPolicy.CLEAR)),
116+
consume(keyPressed(HOME, SHORTCUT_DOWN), (b, e) -> b.model.start(SelectionPolicy.CLEAR)),
117+
consume(keyPressed(END, SHORTCUT_DOWN), (b, e) -> b.model.end(SelectionPolicy.CLEAR)),
115118
// selection
116-
.on(keyPressed(RIGHT, SHIFT_DOWN)).act(StyledTextAreaBehavior::selectRight)
117-
.on(keyPressed(KP_RIGHT, SHIFT_DOWN)).act(StyledTextAreaBehavior::selectRight)
118-
.on(keyPressed(LEFT, SHIFT_DOWN)).act(StyledTextAreaBehavior::selectLeft)
119-
.on(keyPressed(KP_LEFT, SHIFT_DOWN)).act(StyledTextAreaBehavior::selectLeft)
120-
.on(keyPressed(HOME, SHIFT_DOWN)).act((b, e) -> b.model.lineStart(selPolicy))
121-
.on(keyPressed(END, SHIFT_DOWN)).act((b, e) -> b.model.lineEnd(selPolicy))
122-
.on(keyPressed(HOME, SHIFT_DOWN, SHORTCUT_DOWN)).act((b, e) -> b.model.start(selPolicy))
123-
.on(keyPressed(END, SHIFT_DOWN, SHORTCUT_DOWN)).act((b, e) -> b.model.end(selPolicy))
124-
.on(keyPressed(LEFT, SHIFT_DOWN, SHORTCUT_DOWN)).act((b, e) -> b.model.wordBreaksBackwards(2, selPolicy))
125-
.on(keyPressed(KP_LEFT, SHIFT_DOWN, SHORTCUT_DOWN)).act((b, e) -> b.model.wordBreaksBackwards(2, selPolicy))
126-
.on(keyPressed(RIGHT, SHIFT_DOWN, SHORTCUT_DOWN)).act((b, e) -> b.model.wordBreaksForwards(2, selPolicy))
127-
.on(keyPressed(KP_RIGHT, SHIFT_DOWN, SHORTCUT_DOWN)).act((b, e) -> b.model.wordBreaksForwards(2, selPolicy))
128-
.on(keyPressed(A, SHORTCUT_DOWN)).act((b, e) -> b.model.selectAll())
129-
130-
.create();
131-
132-
EventHandlerTemplate<StyledTextAreaBehavior, KeyEvent> otherActions = EventHandlerTemplate
133-
.<StyledTextAreaBehavior, KeyEvent, KeyEvent>
134-
// copy
135-
on(keyPressed(COPY)) .act((b, e) -> b.view.copy())
136-
.on(keyPressed(C, SHORTCUT_DOWN)).act((b, e) -> b.view.copy())
137-
.on(keyPressed(INSERT, SHORTCUT_DOWN)).act((b, e) -> b.view.copy())
138-
.create();
119+
consume(
120+
anyOf(
121+
keyPressed(RIGHT, SHIFT_DOWN),
122+
keyPressed(KP_RIGHT, SHIFT_DOWN)
123+
), StyledTextAreaBehavior::selectRight),
124+
consume(
125+
anyOf(
126+
keyPressed(LEFT, SHIFT_DOWN),
127+
keyPressed(KP_LEFT, SHIFT_DOWN)
128+
), StyledTextAreaBehavior::selectLeft),
129+
consume(keyPressed(HOME, SHIFT_DOWN), (b, e) -> b.model.lineStart(selPolicy)),
130+
consume(keyPressed(END, SHIFT_DOWN), (b, e) -> b.model.lineEnd(selPolicy)),
131+
consume(keyPressed(HOME, SHIFT_DOWN, SHORTCUT_DOWN), (b, e) -> b.model.start(selPolicy)),
132+
consume(keyPressed(END, SHIFT_DOWN, SHORTCUT_DOWN), (b, e) -> b.model.end(selPolicy)),
133+
consume(
134+
anyOf(
135+
keyPressed(RIGHT, SHIFT_DOWN, SHORTCUT_DOWN),
136+
keyPressed(KP_RIGHT, SHIFT_DOWN, SHORTCUT_DOWN)
137+
), (b, e) -> b.model.wordBreaksForwards(2, selPolicy)),
138+
consume(
139+
anyOf(
140+
keyPressed(LEFT, SHIFT_DOWN, SHORTCUT_DOWN),
141+
keyPressed(KP_LEFT, SHIFT_DOWN, SHORTCUT_DOWN)
142+
), (b, e) -> b.model.wordBreaksBackwards(2, selPolicy)),
143+
consume(keyPressed(A, SHORTCUT_DOWN), (b, e) -> b.model.selectAll())
144+
);
145+
146+
InputMapTemplate<StyledTextAreaBehavior, KeyEvent> copyAction = consume(
147+
anyOf(
148+
keyPressed(COPY),
149+
keyPressed(C, SHORTCUT_DOWN),
150+
keyPressed(INSERT, SHORTCUT_DOWN)
151+
), (b, e) -> b.view.copy()
152+
);
139153

140154
Predicate<KeyEvent> noControlKeys = e ->
141155
// filter out control keys
@@ -148,45 +162,29 @@ class StyledTextAreaBehavior implements Behavior {
148162
e.getCode().isDigitKey() ||
149163
e.getCode().isWhitespaceKey();
150164

151-
EventHandlerTemplate<StyledTextAreaBehavior, KeyEvent> charPressConsumer = EventHandlerTemplate
152-
.<StyledTextAreaBehavior, KeyEvent, KeyEvent>
153-
on(keyPressed()).where(isChar.and(noControlKeys)).act((b, e) -> {})
154-
.create();
165+
InputMapTemplate<StyledTextAreaBehavior, KeyEvent> charPressConsumer = consume(keyPressed().onlyIf(isChar.and(noControlKeys)));
155166

156-
KEY_PRESSED_TEMPLATE = edits.orElse(otherNavigation).ifConsumed((b, e) -> b.clearTargetCaretOffset())
167+
InputMapTemplate<StyledTextAreaBehavior, ? super KeyEvent> keyPressedTemplate = edits
168+
.orElse(otherNavigation).ifConsumed((b, e) -> b.clearTargetCaretOffset())
157169
.orElse(verticalNavigation)
158-
.orElse(otherActions)
170+
.orElse(copyAction)
159171
.orElse(charPressConsumer);
160172

161-
KEY_TYPED_TEMPLATE = EventHandlerTemplate
173+
InputMapTemplate<StyledTextAreaBehavior, KeyEvent> keyTypedBase = consume(
162174
// character input
163-
.on(KEY_TYPED)
164-
.where(noControlKeys)
165-
.where(e -> isLegal(e.getCharacter()))
166-
.act(StyledTextAreaBehavior::keyTyped)
167-
168-
.create()
169-
.onlyWhen(b -> b.view.isEditable());
170-
171-
MOUSE_PRESSED_TEMPLATE = EventHandlerTemplate
172-
.on(MouseEvent.MOUSE_PRESSED)
173-
.act(StyledTextAreaBehavior::mousePressed)
174-
.create();
175-
176-
MOUSE_DRAGGED_TEMPLATE = EventHandlerTemplate
177-
.on(MouseEvent.MOUSE_DRAGGED)
178-
.act(StyledTextAreaBehavior::mouseDragged)
179-
.create();
180-
181-
DRAG_DETECTED_TEMPLATE = EventHandlerTemplate
182-
.on(MouseEvent.DRAG_DETECTED)
183-
.act(StyledTextAreaBehavior::dragDetected)
184-
.create();
185-
186-
MOUSE_RELEASED_TEMPLATE = EventHandlerTemplate
187-
.on(MouseEvent.MOUSE_RELEASED)
188-
.act(StyledTextAreaBehavior::mouseReleased)
189-
.create();
175+
EventPattern.keyTyped().onlyIf(noControlKeys.and(e -> isLegal(e.getCharacter()))),
176+
StyledTextAreaBehavior::keyTyped
177+
);
178+
InputMapTemplate<StyledTextAreaBehavior, ? super KeyEvent> keyTypedTemplate = when(b -> b.view.isEditable(), keyTypedBase);
179+
180+
InputMapTemplate<StyledTextAreaBehavior, ? super MouseEvent> mouseEventTemplate = sequence(
181+
consume(eventType(MouseEvent.MOUSE_PRESSED), StyledTextAreaBehavior::mousePressed),
182+
consume(eventType(MouseEvent.MOUSE_DRAGGED), StyledTextAreaBehavior::mouseDragged),
183+
consume(eventType(MouseEvent.DRAG_DETECTED), StyledTextAreaBehavior::dragDetected),
184+
consume(eventType(MouseEvent.MOUSE_RELEASED), StyledTextAreaBehavior::mouseReleased)
185+
);
186+
187+
EVENT_TEMPLATE = sequence(mouseEventTemplate, keyPressedTemplate, keyTypedTemplate);
190188
}
191189

192190
/**
@@ -242,31 +240,8 @@ private CaretOffsetX getTargetCaretOffset() {
242240
this.view = area;
243241
this.model = area.getModel();
244242

245-
EventHandler<? super KeyEvent> keyPressedHandler = KEY_PRESSED_TEMPLATE.bind(this);
246-
EventHandler<? super KeyEvent> keyTypedHandler = KEY_TYPED_TEMPLATE.bind(this);
247-
248-
EventHandler<? super MouseEvent> mousePressedHandler = MOUSE_PRESSED_TEMPLATE.bind(this);
249-
EventHandler<? super MouseEvent> mouseDraggedHandler = MOUSE_DRAGGED_TEMPLATE.bind(this);
250-
EventHandler<? super MouseEvent> dragDetectedHandler = DRAG_DETECTED_TEMPLATE.bind(this);
251-
EventHandler<? super MouseEvent> mouseReleasedHandler = MOUSE_RELEASED_TEMPLATE.bind(this);
252-
253-
EventHandlerHelper.installAfter(area.onKeyPressedProperty(), keyPressedHandler);
254-
EventHandlerHelper.installAfter(area.onKeyTypedProperty(), keyTypedHandler);
255-
256-
EventHandlerHelper.installAfter(area.onMousePressedProperty(), mousePressedHandler);
257-
EventHandlerHelper.installAfter(area.onMouseDraggedProperty(), mouseDraggedHandler);
258-
EventHandlerHelper.installAfter(area.onDragDetectedProperty(), dragDetectedHandler);
259-
EventHandlerHelper.installAfter(area.onMouseReleasedProperty(), mouseReleasedHandler);
260-
261-
subscription = () -> {
262-
EventHandlerHelper.remove(area.onKeyPressedProperty(), keyPressedHandler);
263-
EventHandlerHelper.remove(area.onKeyTypedProperty(), keyTypedHandler);
264-
265-
EventHandlerHelper.remove(area.onMousePressedProperty(), mousePressedHandler);
266-
EventHandlerHelper.remove(area.onMouseDraggedProperty(), mouseDraggedHandler);
267-
EventHandlerHelper.remove(area.onDragDetectedProperty(), dragDetectedHandler);
268-
EventHandlerHelper.remove(area.onMouseReleasedProperty(), mouseReleasedHandler);
269-
};
243+
InputMapTemplate.installFallback(EVENT_TEMPLATE, this, b -> b.view);
244+
subscription = () -> InputMapTemplate.uninstall(EVENT_TEMPLATE, this, b -> b.view);
270245

271246
// setup auto-scroll
272247
Val<Point2D> projection = Val.combine(

0 commit comments

Comments
 (0)