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 "MaterialButton.h"
24 : #include "MaterialLabel.h"
25 : #include "XLScene.h"
26 : #include "MaterialSceneContent.h"
27 : #include "MaterialFloatingMenu.h"
28 : #include "XLInputListener.h"
29 :
30 : namespace STAPPLER_VERSIONIZED stappler::xenolith::material2d {
31 :
32 146 : static SurfaceStyle Button_getSurfaceStyle(NodeStyle style, ColorRole role, uint32_t schemeTag) {
33 292 : return SurfaceStyle(style, Elevation::Level1, ShapeStyle::Full, role, schemeTag);
34 : }
35 :
36 146 : bool Button::init(NodeStyle style, ColorRole role, uint32_t schemeTag) {
37 146 : return init(Button_getSurfaceStyle(style, role, schemeTag));
38 : }
39 :
40 186 : bool Button::init(const SurfaceStyle &style) {
41 186 : if (!Surface::init(style)) {
42 0 : return false;
43 : }
44 :
45 186 : _labelText = addChild(Rc<TypescaleLabel>::create(TypescaleRole::LabelLarge), ZOrder(1));
46 186 : _labelText->setAnchorPoint(Anchor::MiddleLeft);
47 186 : _labelText->setLocaleEnabled(true);
48 186 : _labelText->setOnContentSizeDirtyCallback([this] {
49 76 : updateSizeFromContent();
50 76 : });
51 :
52 186 : _labelValue = addChild(Rc<TypescaleLabel>::create(TypescaleRole::LabelLarge), ZOrder(1));
53 186 : _labelValue->setAnchorPoint(Anchor::MiddleLeft);
54 186 : _labelValue->setLocaleEnabled(true);
55 186 : _labelValue->setOnContentSizeDirtyCallback([this] {
56 40 : updateSizeFromContent();
57 40 : });
58 :
59 186 : _leadingIcon = addChild(Rc<IconSprite>::create(IconName::None), ZOrder(1));
60 186 : _leadingIcon->setAnchorPoint(Anchor::MiddleLeft);
61 186 : _leadingIcon->setContentSize(Size2(18.0f, 18.0f));
62 :
63 186 : _trailingIcon = addChild(Rc<IconSprite>::create(IconName::None), ZOrder(1));
64 186 : _trailingIcon->setAnchorPoint(Anchor::MiddleLeft);
65 186 : _trailingIcon->setContentSize(Size2(18.0f, 18.0f));
66 :
67 186 : _inputListener = addInputListener(Rc<InputListener>::create());
68 186 : _inputListener->addMouseOverRecognizer([this] (const GestureData &data) {
69 1 : _mouseOver = (data.event == GestureEvent::Began);
70 1 : updateActivityState();
71 1 : return true;
72 : });
73 186 : _inputListener->addPressRecognizer([this] (const GesturePress &press) {
74 32 : if (!_enabled || (_menuButtonListener->getSubscription() && !isMenuSourceButtonEnabled())) {
75 0 : return false;
76 : }
77 :
78 32 : if (press.event == GestureEvent::Began) {
79 16 : _touchLocation = press.location();
80 16 : _longPressInit = false;
81 16 : _pressed = true;
82 16 : updateActivityState();
83 16 : } else if (press.event == GestureEvent::Activated) {
84 0 : _longPressInit = true;
85 0 : handleLongPress();
86 0 : _inputListener->setExclusiveForTouch(press.getId());
87 16 : } else if (press.event == GestureEvent::Ended || press.event == GestureEvent::Cancelled) {
88 16 : _pressed = false;
89 16 : updateActivityState();
90 16 : if (press.event == GestureEvent::Ended) {
91 16 : _inputListener->setExclusiveForTouch(press.getId());
92 16 : if (_longPressInit) {
93 : } else {
94 16 : handleTap();
95 : }
96 : }
97 : }
98 32 : return true;
99 : }, LongPressInterval);
100 :
101 186 : _inputListener->addTapRecognizer([this] (const GestureTap &tap) {
102 6 : if (!_enabled) {
103 0 : return false;
104 : }
105 6 : if (tap.count == 2) {
106 0 : _inputListener->setExclusiveForTouch(tap.getId());
107 0 : handleDoubleTap();
108 : }
109 6 : return true;
110 : });
111 :
112 186 : _menuButtonListener = addComponent(Rc<DataListener<MenuSourceButton>>::create([this] (SubscriptionFlags flags) {
113 178 : updateMenuButtonSource();
114 178 : }));
115 :
116 186 : return true;
117 : }
118 :
119 320 : void Button::onContentSizeDirty() {
120 320 : Surface::onContentSizeDirty();
121 :
122 320 : if (_followContentSize && hasContent()) {
123 0 : updateSizeFromContent();
124 : }
125 :
126 320 : layoutContent();
127 320 : }
128 :
129 170 : void Button::setFollowContentSize(bool value) {
130 170 : if (value != _followContentSize) {
131 170 : _followContentSize = value;
132 170 : _contentSizeDirty = true;
133 170 : if (_followContentSize) {
134 0 : updateSizeFromContent();
135 : }
136 : }
137 170 : }
138 :
139 0 : bool Button::isFollowContentSize() const {
140 0 : return _followContentSize;
141 : }
142 :
143 146 : void Button::setSwallowEvents(bool value) {
144 146 : if (value) {
145 146 : _inputListener->setSwallowEvents(InputListener::EventMaskTouch);
146 : } else {
147 0 : _inputListener->clearSwallowEvents(InputListener::EventMaskTouch);
148 : }
149 146 : }
150 :
151 0 : bool Button::isSwallowEvents() const {
152 0 : return _inputListener->isSwallowAllEvents(InputListener::EventMaskTouch);
153 : }
154 :
155 0 : void Button::setEnabled(bool value) {
156 0 : if (value != _enabled) {
157 0 : _enabled = value;
158 0 : _inputListener->setEnabled(_enabled);
159 0 : updateActivityState();
160 : }
161 0 : }
162 :
163 0 : void Button::setSelected(bool val) {
164 0 : if (val != _selected) {
165 0 : _selected = val;
166 0 : updateActivityState();
167 : }
168 0 : }
169 :
170 0 : bool Button::isSelected() const {
171 0 : return _selected;
172 : }
173 :
174 409 : bool Button::isMenuSourceButtonEnabled() const {
175 409 : if (!_menuButtonListener) {
176 0 : return false;
177 : }
178 :
179 409 : return _menuButtonListener->getSubscription()->getCallback() != nullptr || _menuButtonListener->getSubscription()->getNextMenu();
180 : }
181 :
182 130 : void Button::setNodeMask(NodeMask mask) {
183 130 : if (_nodeMask != mask) {
184 130 : _nodeMask = mask;
185 130 : updateSizeFromContent();
186 130 : _contentSizeDirty = true;
187 : }
188 130 : }
189 :
190 354 : void Button::setText(StringView text) {
191 354 : _labelText->setString(text);
192 354 : updateSizeFromContent();
193 354 : _contentSizeDirty = true;
194 354 : }
195 :
196 0 : StringView Button::getText() const {
197 0 : return _labelText->getString8();
198 : }
199 :
200 344 : void Button::setTextValue(StringView text) {
201 344 : _labelValue->setString(text);
202 344 : updateSizeFromContent();
203 344 : _contentSizeDirty = true;
204 344 : }
205 :
206 0 : StringView Button::getTextValue() const {
207 0 : return _labelValue->getString8();
208 : }
209 :
210 130 : void Button::setIconSize(float value) {
211 130 : if (value != getIconSize()) {
212 130 : _leadingIcon->setContentSize(Size2(value, value));
213 130 : _trailingIcon->setContentSize(Size2(value, value));
214 130 : updateSizeFromContent();
215 : }
216 130 : }
217 :
218 696 : float Button::getIconSize() const {
219 696 : return _leadingIcon->getContentSize().width;
220 : }
221 :
222 374 : void Button::setLeadingIconName(IconName name, float progress) {
223 374 : if (name != getLeadingIconName()) {
224 176 : _leadingIcon->setIconName(name);
225 176 : _leadingIcon->setProgress(progress);
226 176 : updateSizeFromContent();
227 : }
228 374 : }
229 :
230 1614 : IconName Button::getLeadingIconName() const {
231 1614 : return _leadingIcon->getIconName();
232 : }
233 :
234 0 : void Button::setLeadingIconProgress(float progress, float animation) {
235 0 : if (animation > 0.0f) {
236 0 : _leadingIcon->animate(progress, animation);
237 : } else {
238 0 : _leadingIcon->setProgress(progress);
239 : }
240 0 : }
241 :
242 0 : float Button::getLeadingIconProgress() const {
243 0 : return _leadingIcon->getProgress();
244 : }
245 :
246 364 : void Button::setTrailingIconName(IconName name) {
247 364 : if (name != getTrailingIconName()) {
248 50 : _trailingIcon->setIconName(name);
249 50 : updateSizeFromContent();
250 : }
251 364 : }
252 :
253 1392 : IconName Button::getTrailingIconName() const {
254 1392 : return _trailingIcon->getIconName();
255 : }
256 :
257 0 : void Button::setTrailingIconProgress(float progress, float animation) {
258 0 : if (animation > 0.0f) {
259 0 : _trailingIcon->animate(progress, animation);
260 : } else {
261 0 : _trailingIcon->setProgress(progress);
262 : }
263 0 : }
264 :
265 0 : float Button::getTrailingIconProgress() const {
266 0 : return _trailingIcon->getProgress();
267 : }
268 :
269 66 : void Button::setTapCallback(Function<void()> &&cb) {
270 66 : _callbackTap = move(cb);
271 66 : }
272 :
273 0 : void Button::setLongPressCallback(Function<void()> &&cb) {
274 0 : _callbackLongPress = move(cb);
275 0 : }
276 :
277 0 : void Button::setDoubleTapCallback(Function<void()> &&cb) {
278 0 : _callbackDoubleTap = move(cb);
279 0 : }
280 :
281 166 : void Button::setMenuSourceButton(Rc<MenuSourceButton> &&button) {
282 166 : if (button != _menuButtonListener->getSubscription()) {
283 166 : if (auto b = _menuButtonListener->getSubscription()) {
284 0 : b->handleNodeDetached(this);
285 : }
286 166 : _menuButtonListener->setSubscription(button.get());
287 166 : updateMenuButtonSource();
288 166 : if (button) {
289 166 : button->handleNodeAttached(this);
290 : }
291 : }
292 166 : }
293 :
294 0 : MenuSourceButton *Button::getMenuSourceButton() const {
295 0 : return _menuButtonListener->getSubscription();
296 : }
297 :
298 0 : void Button::setBlendColor(ColorRole rule, float value) {
299 0 : _labelText->setBlendColor(rule, value);
300 0 : _labelValue->setBlendColor(rule, value);
301 0 : _leadingIcon->setBlendColor(rule, value);
302 0 : _trailingIcon->setBlendColor(rule, value);
303 0 : }
304 :
305 10 : void Button::setBlendColor(const Color4F &color, float value) {
306 10 : _labelText->setBlendColor(color, value);
307 10 : _labelValue->setBlendColor(color, value);
308 10 : _leadingIcon->setBlendColor(color, value);
309 10 : _trailingIcon->setBlendColor(color, value);
310 10 : }
311 :
312 0 : ColorRole Button::getBlendColorRule() const {
313 0 : return _labelText->getBlendColorRule();
314 : }
315 :
316 0 : const Color4F &Button::getBlendColor() const {
317 0 : return _labelText->getBlendColor();
318 : }
319 :
320 0 : float Button::getBlendColorValue() const {
321 0 : return _labelText->getBlendColorValue();
322 : }
323 :
324 678 : bool Button::hasContent() const {
325 1244 : return !_labelText->empty() || !_labelValue->empty()
326 566 : || _leadingIcon->getIconName() != IconName::None
327 1244 : || _trailingIcon->getIconName() != IconName::None;
328 : }
329 :
330 1300 : void Button::updateSizeFromContent() {
331 1300 : if (!_followContentSize || !hasContent()) {
332 622 : _contentSizeDirty = true;
333 622 : return;
334 : }
335 :
336 678 : Size2 targetSize;
337 678 : if (!_labelText->empty() && (_nodeMask & NodeMask::LabelText) != NodeMask::None) {
338 112 : targetSize = _labelText->getContentSize();
339 : } else {
340 566 : targetSize.height = getIconSize();
341 : }
342 678 : targetSize.width = getWidthForContent();
343 678 : targetSize.height += 24.0f;
344 :
345 678 : setContentSize(targetSize);
346 : }
347 :
348 377 : void Button::updateActivityState() {
349 377 : auto style = getStyleTarget();
350 377 : if (!_enabled || (_menuButtonListener->getSubscription() && !isMenuSourceButtonEnabled())) {
351 20 : style.activityState = ActivityState::Disabled;
352 357 : } else if (_pressed || _selected) {
353 26 : style.activityState = ActivityState::Pressed;
354 331 : } else if (_mouseOver) {
355 1 : style.activityState = ActivityState::Hovered;
356 330 : } else if (_focused) {
357 0 : style.activityState = ActivityState::Focused;
358 : } else {
359 330 : style.activityState = ActivityState::Enabled;
360 : }
361 377 : setStyle(style, _activityAnimationDuration);
362 377 : }
363 :
364 16 : void Button::handleTap() {
365 16 : if (_callbackTap) {
366 16 : auto id = retain();
367 16 : _callbackTap();
368 16 : release(id);
369 0 : } else if (auto btn = _menuButtonListener->getSubscription()) {
370 0 : auto &cb = btn->getCallback();
371 0 : if (cb) {
372 0 : cb(this, btn);
373 0 : } else if (_floatingMenuSource) {
374 0 : if (auto content = dynamic_cast<SceneContent2d *>(_scene->getContent())) {
375 0 : auto posRight = content->convertToNodeSpace(convertToWorldSpace(Vec2(_contentSize.width, _contentSize.height)));
376 :
377 0 : FloatingMenu::push(content, _floatingMenuSource, posRight,
378 : FloatingMenu::Binding::OriginRight, nullptr);
379 : }
380 : }
381 : }
382 16 : }
383 :
384 0 : void Button::handleLongPress() {
385 0 : if (_callbackLongPress) {
386 0 : auto id = retain();
387 0 : _callbackLongPress();
388 0 : release(id);
389 : }
390 0 : }
391 :
392 0 : void Button::handleDoubleTap() {
393 0 : if (_callbackDoubleTap) {
394 0 : auto id = retain();
395 0 : _callbackDoubleTap();
396 0 : release(id);
397 : }
398 0 : }
399 :
400 678 : float Button::getWidthForContent() const {
401 678 : float contentWidth = 0.0f;
402 678 : if (!_labelText->empty() && (_nodeMask & NodeMask::LabelText) != NodeMask::None) {
403 112 : contentWidth = ((_styleTarget.nodeStyle == NodeStyle::Text) ? 24.0f : 48.0f) + _labelText->getContentSize().width;
404 112 : if (_styleTarget.nodeStyle == NodeStyle::Text && (getLeadingIconName() != IconName::None || getTrailingIconName() != IconName::None)) {
405 92 : contentWidth += 16.0f;
406 : }
407 : } else {
408 566 : contentWidth = 24.0f;
409 : }
410 :
411 678 : if (!_labelValue->empty() && (_nodeMask & NodeMask::LabelValue) != NodeMask::None) {
412 30 : contentWidth += _labelValue->getContentSize().width + 8.0f;
413 : }
414 :
415 678 : if (getLeadingIconName() != IconName::None && (_nodeMask & NodeMask::LeadingIcon) != NodeMask::None) {
416 628 : contentWidth += _leadingIcon->getContentSize().width;
417 : }
418 678 : if (getTrailingIconName() != IconName::None && (_nodeMask & NodeMask::TrailingIcon) != NodeMask::None) {
419 70 : contentWidth += _trailingIcon->getContentSize().width;
420 : }
421 678 : return contentWidth;
422 : }
423 :
424 344 : void Button::updateMenuButtonSource() {
425 344 : if (auto btn = _menuButtonListener->getSubscription()) {
426 344 : _selected = btn->isSelected();
427 344 : _floatingMenuSource = btn->getNextMenu();
428 :
429 344 : setLeadingIconName(btn->getNameIcon());
430 344 : setTrailingIconName(btn->getValueIcon());
431 344 : setText(btn->getName());
432 344 : setTextValue(btn->getValue());
433 :
434 344 : if (btn->getNextMenu()) {
435 20 : if (getTrailingIconName() == IconName::None) {
436 20 : setTrailingIconName(IconName::Navigation_arrow_right_solid);
437 : }
438 : }
439 : } else {
440 0 : _selected = false;
441 0 : _floatingMenuSource = nullptr;
442 : }
443 344 : updateActivityState();
444 344 : }
445 :
446 170 : void Button::layoutContent() {
447 170 : auto leadingIconName = getLeadingIconName();
448 170 : auto trailingIconName = getTrailingIconName();
449 170 : auto hasLabelText = !_labelText->empty();
450 170 : auto hasLabelValue = !_labelValue->empty();
451 :
452 170 : _labelText->setVisible((_nodeMask & NodeMask::LabelText) != NodeMask::None);
453 170 : _labelValue->setVisible((_nodeMask & NodeMask::LabelValue) != NodeMask::None);
454 170 : _leadingIcon->setVisible((_nodeMask & NodeMask::LeadingIcon) != NodeMask::None);
455 170 : _trailingIcon->setVisible((_nodeMask & NodeMask::TrailingIcon) != NodeMask::None);
456 :
457 170 : if ((_nodeMask & NodeMask::LabelText) == NodeMask::None) {
458 170 : hasLabelText = false;
459 : }
460 :
461 170 : if ((_nodeMask & NodeMask::LabelValue) == NodeMask::None) {
462 170 : hasLabelValue = false;
463 : }
464 :
465 170 : if ((_nodeMask & NodeMask::LeadingIcon) == NodeMask::None) {
466 0 : leadingIconName = IconName::None;
467 : }
468 :
469 170 : if ((_nodeMask & NodeMask::TrailingIcon) == NodeMask::None) {
470 170 : trailingIconName = IconName::None;
471 : }
472 :
473 170 : if (leadingIconName != IconName::None && trailingIconName == IconName::None && !hasLabelText && !hasLabelValue) {
474 170 : _leadingIcon->setAnchorPoint(Anchor::Middle);
475 170 : _leadingIcon->setPosition(_contentSize / 2.0f);
476 : } else {
477 0 : _leadingIcon->setAnchorPoint(Anchor::MiddleLeft);
478 :
479 0 : float contentWidth = getWidthForContent();
480 0 : float offset = (_contentSize.width - contentWidth) / 2.0f;
481 :
482 0 : Vec2 target(offset + (_styleTarget.nodeStyle == NodeStyle::Text ? 12.0f : 16.0f), _contentSize.height / 2.0f);
483 :
484 0 : if (leadingIconName != IconName::None) {
485 0 : _leadingIcon->setPosition(target);
486 0 : target.x += 8.0f + _leadingIcon->getContentSize().width;
487 0 : } else if (_styleTarget.nodeStyle != NodeStyle::Text) {
488 0 : target.x += 8.0f;
489 : }
490 :
491 0 : _labelText->setPosition(target);
492 0 : target.x += _labelText->getContentSize().width + 8.0f;
493 :
494 0 : if (hasLabelValue) {
495 0 : _labelValue->setPosition(target);
496 0 : target.x += _labelValue->getContentSize().width + 8.0f;
497 : }
498 :
499 0 : _trailingIcon->setPosition(target);
500 : }
501 170 : }
502 :
503 : }
|