Line data Source code
1 : /**
2 : Copyright (c) 2023 Stappler LLC <admin@stappler.dev>
3 :
4 : Permission is hereby granted, free of charge, to any person obtaining a copy
5 : of this software and associated documentation files (the "Software"), to deal
6 : in the Software without restriction, including without limitation the rights
7 : to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 : copies of the Software, and to permit persons to whom the Software is
9 : furnished to do so, subject to the following conditions:
10 :
11 : The above copyright notice and this permission notice shall be included in
12 : all copies or substantial portions of the Software.
13 :
14 : THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 : IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 : FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 : AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 : LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 : OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 : THE SOFTWARE.
21 : **/
22 :
23 : #include "MaterialInputTextContainer.h"
24 : #include "MaterialSurfaceInterior.h"
25 : #include "MaterialStyleContainer.h"
26 : #include "MaterialEasing.h"
27 : #include "XLFrameInfo.h"
28 : #include "XL2dLayer.h"
29 : #include "XLAction.h"
30 : #include "XL2dVectorSprite.h"
31 :
32 : namespace STAPPLER_VERSIONIZED stappler::xenolith::material2d {
33 :
34 20 : InputTextContainer::~InputTextContainer() { }
35 :
36 10 : bool InputTextContainer::init() {
37 10 : if (!DynamicStateNode::init()) {
38 0 : return false;
39 : }
40 :
41 10 : _label = addChild(Rc<TypescaleLabel>::create(TypescaleRole::BodyLarge), ZOrder(-1));
42 10 : _label->setAnchorPoint(Anchor::BottomLeft);
43 :
44 10 : _label->setOnTransformDirtyCallback([this] (const Mat4 &) {
45 80 : updateCursorPointers();
46 80 : });
47 :
48 10 : _caret = _label->addChild(Rc<Layer>::create());
49 10 : _caret->setAnchorPoint(Anchor::BottomLeft);
50 10 : _caret->setOpacity(0.0f);
51 :
52 10 : _cursorPointer = addChild(Rc<IconSprite>::create(IconName::Stappler_CursorIcon), ZOrder(1));
53 10 : _cursorPointer->setContentSize(Size2(24.0f, 24.0f));
54 10 : _cursorPointer->setAnchorPoint(Vec2(0.5f, _cursorAnchor));
55 10 : _cursorPointer->setBlendColor(ColorRole::Primary, 1.0f);
56 10 : _cursorPointer->setVisible(false);
57 :
58 10 : _selectionPointerStart = addChild(Rc<IconSprite>::create(IconName::Stappler_SelectioinStartIcon), ZOrder(1));
59 10 : _selectionPointerStart->setContentSize(Size2(24.0f, 24.0f));
60 10 : _selectionPointerStart->setAnchorPoint(Vec2(1.0f, _cursorAnchor));
61 10 : _selectionPointerStart->setBlendColor(ColorRole::Primary, 1.0f);
62 10 : _selectionPointerStart->setVisible(false);
63 :
64 10 : _selectionPointerEnd = addChild(Rc<IconSprite>::create(IconName::Stappler_SelectioinEndIcon), ZOrder(1));
65 10 : _selectionPointerEnd->setContentSize(Size2(24.0f, 24.0f));
66 10 : _selectionPointerEnd->setAnchorPoint(Vec2(0.0f, _cursorAnchor));
67 10 : _selectionPointerEnd->setBlendColor(ColorRole::Primary, 1.0f);
68 10 : _selectionPointerEnd->setVisible(false);
69 :
70 10 : setStateApplyMode(StateApplyMode::ApplyForNodesBelow);
71 10 : enableScissor(Padding(0.0f, 2.0f));
72 :
73 10 : return true;
74 : }
75 :
76 0 : void InputTextContainer::update(const UpdateTime &time) {
77 0 : DynamicStateNode::update(time);
78 :
79 0 : if (_selectedPointer) {
80 0 : if (hasHorizontalOverflow()) {
81 0 : const auto width = _contentSize.width;
82 0 : const auto offset = std::min(48.0f, width / 3.0f);
83 0 : const auto xPos = _selectedPointer->getPosition().x;
84 0 : const auto labelWidth = _label->getContentSize().width;
85 0 : const auto labelPos = _label->getPosition().x;
86 0 : const auto minPos = width - labelWidth;
87 0 : const auto maxPos = 0.0f;
88 :
89 0 : const auto maxv = 300.0f;
90 :
91 0 : if (xPos < offset) {
92 0 : const float relPos = 1.0f - math::clamp(xPos / offset, 0.0f, 1.0f);
93 0 : _label->setPositionX(std::min(maxPos, labelPos + relPos * maxv * time.dt));
94 0 : } else if (xPos > width - offset) {
95 0 : const float relPos = 1.0f - math::clamp((width - xPos) / offset, 0.0f, 1.0f);
96 0 : _label->setPositionX(std::max(minPos, labelPos - relPos * maxv * time.dt));
97 : }
98 : }
99 : }
100 0 : }
101 :
102 10 : void InputTextContainer::onContentSizeDirty() {
103 10 : DynamicStateNode::onContentSizeDirty();
104 :
105 10 : _label->setPosition(Vec2(0.0f, 0.0f) + Vec2(_label->getContentSize() - _contentSize) * _adjustment);
106 10 : _caret->setContentSize(Size2(1.5f, _label->getFontHeight()));
107 10 : }
108 :
109 370 : bool InputTextContainer::visitDraw(FrameInfo &frame, NodeFlags parentFlags) {
110 370 : if (!_visible) {
111 0 : return false;
112 : }
113 :
114 370 : if (_cursorDirty) {
115 80 : updateCursorPosition();
116 80 : _cursorDirty = false;
117 : }
118 :
119 370 : auto style = frame.getComponent<SurfaceInterior>(SurfaceInterior::ComponentFrameTag);
120 370 : auto styleContainer = frame.getComponent<StyleContainer>(StyleContainer::ComponentFrameTag);
121 370 : if (style && styleContainer) {
122 370 : if (auto scheme = styleContainer->getScheme(style->getStyle().schemeTag)) {
123 370 : auto c = scheme->get(ColorRole::Primary);
124 370 : auto currentColor = _caret->getColor();
125 370 : currentColor.a = 1.0f;
126 370 : if (currentColor != c) {
127 10 : _caret->setColor(c, false);
128 : }
129 : }
130 : }
131 :
132 370 : return DynamicStateNode::visitDraw(frame, parentFlags);
133 370 : }
134 :
135 20 : void InputTextContainer::setEnabled(bool value) {
136 20 : if (value != _enabled) {
137 20 : _enabled = value;
138 20 : _caret->stopAllActions();
139 20 : _caret->runAction(makeEasing(Rc<FadeTo>::create(0.2f, _enabled ? 1.0f : 0.0f)));
140 :
141 20 : if (!_enabled) {
142 10 : unscheduleCursorPointer();
143 10 : stopActionByTag("RenderContinuously"_tag);
144 10 : setPointerEnabled(false);
145 : }
146 : }
147 20 : }
148 :
149 340 : void InputTextContainer::setCursor(TextCursor cursor) {
150 340 : if (_cursor != cursor) {
151 340 : _cursor = cursor;
152 340 : _cursorDirty = true;
153 340 : _caret->setVisible(_cursor.length == 0);
154 340 : if (_cursor.length > 0) {
155 10 : _label->setSelectionCursor(_cursor);
156 : } else {
157 330 : _label->setSelectionCursor(TextCursor::InvalidCursor);
158 : }
159 : }
160 340 : }
161 :
162 330 : void InputTextContainer::handleLabelChanged() {
163 330 : _cursorDirty = true;
164 330 : unscheduleCursorPointer();
165 330 : setPointerEnabled(false);
166 330 : }
167 :
168 10 : TextCursor InputTextContainer::getCursorForPosition(const Vec2 &loc) {
169 10 : if (_label->isTouched(loc, 4)) {
170 0 : auto chIdx = _label->getCharIndex(_label->convertToNodeSpace(loc));
171 0 : if (chIdx.first != maxOf<uint32_t>()) {
172 0 : if (chIdx.second) {
173 0 : return TextCursor(chIdx.first + 1);
174 : } else {
175 0 : return TextCursor(chIdx.first);
176 : }
177 : }
178 : }
179 10 : return TextCursor::InvalidCursor;
180 : }
181 :
182 0 : bool InputTextContainer::hasHorizontalOverflow() const {
183 0 : return _label->getContentSize().width > _contentSize.width;
184 : }
185 :
186 0 : void InputTextContainer::moveHorizontalOverflow(float d) {
187 0 : _label->stopAllActionsByTag("InputTextContainerAdjust"_tag);
188 :
189 0 : const auto labelWidth = _label->getContentSize().width;
190 0 : const auto width = _contentSize.width;
191 0 : const auto minPos = width - labelWidth;
192 0 : const auto maxPos = 0.0f;
193 :
194 0 : auto newpos = _label->getPosition().x + d;
195 0 : if (newpos < minPos) {
196 0 : newpos = minPos;
197 0 : } else if (newpos > maxPos) {
198 0 : newpos = maxPos;
199 : }
200 :
201 0 : _label->setPositionX(newpos);
202 0 : }
203 :
204 0 : IconSprite *InputTextContainer::getTouchedCursor(const Vec2 &vec, float padding) {
205 0 : if (_cursorPointer->isVisible() && _cursorPointer->isTouched(vec, padding)) {
206 0 : return _cursorPointer;
207 : }
208 0 : if (_selectionPointerStart->isVisible() && _selectionPointerStart->isTouched(vec, padding)) {
209 0 : return _selectionPointerStart;
210 : }
211 0 : if (_selectionPointerEnd->isVisible() && _selectionPointerEnd->isTouched(vec, padding)) {
212 0 : return _selectionPointerEnd;
213 : }
214 0 : return nullptr;
215 : }
216 :
217 0 : bool InputTextContainer::handleLongPress(const Vec2 &pt, uint32_t tickCount) {
218 0 : if (tickCount == 1) {
219 0 : if (_selectedPointer
220 0 : || (_cursorPointer->isVisible() && _cursorPointer->getOpacity() > 0.0f && _cursorPointer->isTouched(pt))
221 0 : || (_selectionPointerStart->isVisible() && _selectionPointerStart->getOpacity() > 0.0f && _selectionPointerStart->isTouched(pt))
222 0 : || (_selectionPointerEnd->isVisible() && _selectionPointerEnd->getOpacity() > 0.0f && _selectionPointerEnd->isTouched(pt))) {
223 0 : return false;
224 : }
225 :
226 0 : auto pos = _label->convertToNodeSpace(pt);
227 :
228 0 : auto chIdx = _label->getCharIndex(pos, font::CharSelectMode::Center);
229 0 : if (chIdx.first != maxOf<uint32_t>()) {
230 0 : auto word = _label->selectWord(chIdx.first);
231 0 : setCursor(word);
232 0 : if (_cursorCallback) {
233 0 : _cursorCallback(_cursor);
234 : }
235 0 : scheduleCursorPointer();
236 0 : return true;
237 : }
238 :
239 0 : } else if (tickCount == 3) {
240 0 : setCursor(TextCursor(0, uint32_t(_label->getCharsCount())));
241 0 : if (_cursorCallback) {
242 0 : _cursorCallback(_cursor);
243 : }
244 0 : scheduleCursorPointer();
245 : }
246 0 : return false;
247 : }
248 :
249 0 : bool InputTextContainer::handleSwipeBegin(const Vec2 &pt) {
250 0 : auto c = getTouchedCursor(pt);
251 0 : if (c != nullptr) {
252 0 : unscheduleCursorPointer();
253 0 : _selectedPointer = c;
254 0 : scheduleUpdate();
255 0 : runAction(Rc<RenderContinuously>::create(), "RenderContinuously"_tag);
256 0 : return true;
257 : }
258 0 : return false;
259 : }
260 :
261 0 : bool InputTextContainer::handleSwipe(const Vec2 &pt, const Vec2 &delta) {
262 0 : if (_selectedPointer) {
263 0 : unscheduleCursorPointer();
264 0 : auto size = _selectedPointer->getContentSize();
265 0 : auto anchor = _selectedPointer->getAnchorPoint();
266 0 : auto offset = Vec2(anchor.x * size.width - size.width / 2.0f, (anchor.y + 1.0f) * size.height);
267 :
268 0 : auto locInLabel = _label->convertToNodeSpace(pt) + offset;
269 :
270 0 : if (_selectedPointer == _cursorPointer) {
271 0 : auto chIdx = _label->getCharIndex(locInLabel);
272 0 : if (chIdx.first != maxOf<uint32_t>()) {
273 0 : auto cursorIdx = chIdx.first;
274 0 : if (chIdx.second) {
275 0 : ++ cursorIdx;
276 : }
277 :
278 0 : if (_cursor.start != cursorIdx) {
279 0 : setCursor(TextCursor(cursorIdx));
280 0 : if (_cursorCallback) {
281 0 : _cursorCallback(_cursor);
282 : }
283 : }
284 : }
285 0 : } else if (_selectedPointer == _selectionPointerStart) {
286 0 : auto charNumber = _label->getCharIndex(locInLabel, font::CharSelectMode::Prefix).first;
287 0 : if (charNumber != maxOf<uint32_t>()) {
288 0 : if (charNumber != _cursor.start && charNumber < _cursor.start + _cursor.length) {
289 0 : setCursor(TextCursor(charNumber, (_cursor.start + _cursor.length) - charNumber));
290 : }
291 : }
292 0 : } else if (_selectedPointer == _selectionPointerEnd) {
293 0 : auto charNumber = _label->getCharIndex(locInLabel, font::CharSelectMode::Suffix).first;
294 0 : if (charNumber != maxOf<uint32_t>()) {
295 0 : if (charNumber != _cursor.start + _cursor.length - 1 && charNumber >= _cursor.start) {
296 0 : setCursor(TextCursor(_cursor.start, charNumber - _cursor.start + 1));
297 : }
298 : }
299 : }
300 0 : return true;
301 : }
302 0 : return false;
303 : }
304 :
305 0 : bool InputTextContainer::handleSwipeEnd(const Vec2 &pt) {
306 0 : if (_selectedPointer) {
307 0 : _selectedPointer = nullptr;
308 0 : if (_scheduled && _running) {
309 0 : stopActionByTag("RenderContinuously"_tag);
310 0 : unscheduleUpdate();
311 : }
312 0 : scheduleCursorPointer();
313 0 : return true;
314 : }
315 0 : return false;
316 : }
317 :
318 10 : void InputTextContainer::touchPointers() {
319 10 : if (!_label->empty()) {
320 0 : scheduleCursorPointer();
321 : }
322 10 : }
323 :
324 30 : void InputTextContainer::setCursorCallback(Function<void(TextCursor)> &&cb) {
325 30 : _cursorCallback = move(cb);
326 30 : }
327 :
328 0 : const Function<void(TextCursor)> &InputTextContainer::getCursorCallback() const {
329 0 : return _cursorCallback;
330 : }
331 :
332 80 : void InputTextContainer::updateCursorPosition() {
333 80 : Vec2 cpos;
334 80 : if (_label->empty()) {
335 10 : cpos = Vec2(0.0f, 0.0f);
336 : } else {
337 70 : cpos = _label->getCursorPosition(_cursor.start);
338 : }
339 80 : _caret->setPosition(cpos);
340 :
341 80 : const auto labelWidth = _label->getContentSize().width;
342 80 : const auto width = _contentSize.width;
343 80 : const auto minPos = width - std::max(labelWidth, cpos.x);
344 80 : const auto maxPos = 0.0f;
345 :
346 80 : if (labelWidth <= width) {
347 80 : runAdjustLabel(0.0f);
348 : } else {
349 0 : auto maxWidth = std::min(width / 4.0f, 60.0f);
350 0 : auto containerPos = _label->getNodeToParentTransform().transformPoint(cpos);
351 0 : if (containerPos.x < maxWidth || containerPos.x > width - maxWidth) {
352 0 : auto newpos = width / 2.0f - cpos.x;
353 0 : if (newpos < minPos) {
354 0 : newpos = minPos;
355 0 : } else if (newpos > maxPos) {
356 0 : newpos = maxPos;
357 : }
358 :
359 0 : runAdjustLabel(newpos);
360 : }
361 : }
362 :
363 80 : updateCursorPointers();
364 80 : }
365 :
366 160 : void InputTextContainer::updateCursorPointers() {
367 160 : const auto width = _contentSize.width;
368 :
369 160 : auto &t = _label->getNodeToParentTransform();
370 :
371 160 : if (_cursor.length > 0) {
372 20 : auto endPos = t.transformPoint(_label->getCursorPosition(_cursor.start + _cursor.length - 1, false));
373 20 : _selectionPointerEnd->setPosition(endPos + Vec2(_caret->getContentSize().width / 2.0f, 0.0f));
374 :
375 20 : if (endPos.x >= 0.0f && endPos.x <= width) {
376 20 : _selectionPointerEnd->setOpacity(1.0f);
377 0 : } else if (endPos.x < 0.0f) {
378 0 : auto op = math::clamp((endPos.x + 10.0f) / 10.0f, 0.0f, 1.0f);
379 0 : _selectionPointerEnd->setOpacity(op);
380 0 : } else if (endPos.x > width) {
381 0 : auto op = math::clamp((width - endPos.x + 10.0f) / 10.0f, 0.0f, 1.0f);
382 0 : _selectionPointerEnd->setOpacity(op);
383 : }
384 : }
385 :
386 160 : auto cursorPos = t.transformPoint(_label->getCursorPosition(_cursor.start, true));
387 :
388 160 : _cursorPointer->setPosition(cursorPos + Vec2(_caret->getContentSize().width / 2.0f, 0.0f));
389 160 : _selectionPointerStart->setPosition(cursorPos + Vec2(_caret->getContentSize().width / 2.0f, 0.0f));
390 :
391 160 : if (cursorPos.x >= 0.0f && cursorPos.x <= width) {
392 160 : _cursorPointer->setOpacity(1.0f);
393 160 : _selectionPointerStart->setOpacity(1.0f);
394 0 : } else if (cursorPos.x < 0.0f) {
395 0 : auto op = math::clamp((cursorPos.x + 10.0f) / 10.0f, 0.0f, 1.0f);
396 0 : _cursorPointer->setOpacity(op);
397 0 : _selectionPointerStart->setOpacity(op);
398 0 : } else if (cursorPos.x > width) {
399 0 : auto op = math::clamp((width - cursorPos.x + 10.0f) / 10.0f, 0.0f, 1.0f);
400 0 : _cursorPointer->setOpacity(op);
401 0 : _selectionPointerStart->setOpacity(op);
402 : }
403 :
404 160 : if (_pointerEnabled && !_label->empty()) {
405 0 : if (_cursor.length == 0) {
406 0 : _cursorPointer->setVisible(true);
407 0 : _selectionPointerStart->setVisible(false);
408 0 : _selectionPointerEnd->setVisible(false);
409 : } else {
410 0 : _cursorPointer->setVisible(false);
411 0 : _selectionPointerStart->setVisible(true);
412 0 : _selectionPointerEnd->setVisible(true);
413 : }
414 : } else {
415 160 : _cursorPointer->setVisible(false);
416 160 : _selectionPointerStart->setVisible(false);
417 160 : _selectionPointerEnd->setVisible(false);
418 : }
419 160 : }
420 :
421 80 : void InputTextContainer::runAdjustLabel(float pos) {
422 80 : if (_selectedPointer) {
423 80 : return;
424 : }
425 :
426 80 : if (_label->getPosition().x == pos) {
427 80 : _label->stopAllActionsByTag("InputTextContainerAdjust"_tag);
428 80 : return;
429 : }
430 :
431 0 : _label->stopAllActionsByTag("InputTextContainerAdjust"_tag);
432 :
433 0 : const float minT = 0.05f;
434 0 : const float maxT = 0.35f;
435 0 : const float labelPos = _label->getPosition().x;
436 :
437 0 : auto dist = std::fabs(labelPos - pos);
438 :
439 0 : if (_enabled) {
440 0 : if (dist > _contentSize.width * 0.5f) {
441 0 : auto targetPos = labelPos - std::copysign(dist - _contentSize.width * 0.25f, labelPos - pos);
442 0 : _label->setPositionX(targetPos);
443 0 : dist = _contentSize.width * 0.5f;
444 : }
445 : }
446 :
447 0 : auto t = minT;
448 0 : if (dist < 16.0f) {
449 0 : t = minT;
450 0 : } else if (dist > 80.0f) {
451 0 : t = maxT;
452 : } else {
453 0 : t = progress(minT, maxT, (dist - 16.0f) / 80.0f);
454 : }
455 :
456 0 : auto a = makeEasing(Rc<MoveTo>::create(t, Vec2(pos, _label->getPosition().y)));
457 0 : _label->runAction(a, "InputTextContainerAdjust"_tag);
458 0 : }
459 :
460 0 : void InputTextContainer::scheduleCursorPointer() {
461 0 : stopAllActionsByTag("TextFieldCursorPointer"_tag);
462 0 : setPointerEnabled(true);
463 0 : if (_cursor.length == 0) {
464 0 : runAction(Rc<Sequence>::create(3.5f, [this] {
465 0 : setPointerEnabled(false);
466 0 : }), "TextFieldCursorPointer"_tag);
467 : }
468 0 : }
469 :
470 340 : void InputTextContainer::unscheduleCursorPointer() {
471 340 : stopAllActionsByTag("TextFieldCursorPointer"_tag);
472 340 : }
473 :
474 340 : void InputTextContainer::setPointerEnabled(bool value) {
475 340 : if (_pointerEnabled != value) {
476 0 : _pointerEnabled = value;
477 0 : updateCursorPointers();
478 : }
479 340 : }
480 :
481 : }
|