22
33import static javafx .scene .input .KeyCode .*;
44import static javafx .scene .input .KeyCombination .*;
5- import static javafx .scene .input .KeyEvent .*;
65import 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 ;
810import static org .reactfx .EventStreams .*;
911
1012import java .util .Optional ;
1113import java .util .function .Predicate ;
1214
13- import javafx .event .EventHandler ;
15+ import javafx .event .Event ;
1416import javafx .geometry .Bounds ;
1517import javafx .geometry .Point2D ;
1618import javafx .scene .control .IndexRange ;
2123import org .fxmisc .richtext .NavigationActions .SelectionPolicy ;
2224import org .fxmisc .richtext .TwoDimensional .Position ;
2325import 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 ;
2628import org .fxmisc .wellbehaved .skin .Behavior ;
2729import org .reactfx .EventStream ;
2830import 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