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 "MaterialAppBar.h"
24 : #include "MaterialButton.h"
25 : #include "XLInputListener.h"
26 : #include "XLAction.h"
27 :
28 : namespace STAPPLER_VERSIONIZED stappler::xenolith::material2d {
29 :
30 10 : bool AppBar::init(AppBarLayout layout, const SurfaceStyle & style) {
31 10 : if (!Surface::init(style)) {
32 0 : return false;
33 : }
34 :
35 10 : _layout = layout;
36 :
37 10 : _inputListener = addInputListener(Rc<InputListener>::create());
38 10 : _inputListener->addTouchRecognizer([this] (const GestureData &) -> bool {
39 0 : return isSwallowTouches();
40 : });
41 10 : _inputListener->addPressRecognizer([this] (const GesturePress &press) -> bool {
42 0 : if (_barCallback) {
43 0 : if (press.event == GestureEvent::Ended) {
44 0 : _barCallback();
45 : }
46 0 : return true;
47 : }
48 0 : return false;
49 10 : }, TimeInterval::milliseconds(425), true);
50 10 : _inputListener->setSwallowEvents(InputListener::EventMaskTouch);
51 :
52 10 : _actionMenuSourceListener = addComponent(Rc<DataListener<MenuSource>>::create(std::bind(&AppBar::layoutSubviews, this)));
53 :
54 10 : _navButton = addChild(Rc<Button>::create(NodeStyle::Text), ZOrder(1));
55 10 : _navButton->setTapCallback(std::bind(&AppBar::handleNavTapped, this));
56 10 : _navButton->setLeadingIconName(IconName::Navigation_menu_solid);
57 10 : _navButton->setIconSize(24.0f);
58 10 : _navButton->setNodeMask(Button::LeadingIcon);
59 10 : _navButton->setFollowContentSize(false);
60 10 : _navButton->setSwallowEvents(true);
61 :
62 10 : _label = addChild(Rc<TypescaleLabel>::create(TypescaleRole::TitleLarge));
63 10 : _label->setAnchorPoint(Anchor::MiddleLeft);
64 :
65 10 : _scissorNode = addChild(Rc<DynamicStateNode>::create());
66 10 : _scissorNode->setPosition(Vec2(0, 0));
67 10 : _scissorNode->setAnchorPoint(Anchor::BottomLeft);
68 :
69 10 : _iconsComposer = addChild(Rc<Node>::create(), ZOrder(1));
70 10 : _iconsComposer->setPosition(Vec2(0, 0));
71 10 : _iconsComposer->setAnchorPoint(Anchor::BottomLeft);
72 10 : _iconsComposer->setCascadeOpacityEnabled(true);
73 :
74 10 : updateDefaultHeight();
75 :
76 10 : setStateApplyMode(StateApplyMode::ApplyForAll);
77 10 : enableScissor();
78 :
79 10 : return true;
80 : }
81 :
82 20 : void AppBar::onContentSizeDirty() {
83 20 : Surface::onContentSizeDirty();
84 20 : updateDefaultHeight();
85 20 : layoutSubviews();
86 20 : }
87 :
88 0 : void AppBar::setLayout(AppBarLayout layout) {
89 0 : if (_layout != layout) {
90 0 : _layout = layout;
91 0 : _contentSizeDirty = true;
92 : }
93 0 : }
94 :
95 10 : void AppBar::setTitle(StringView str) {
96 10 : _label->setString(str);
97 10 : }
98 :
99 0 : StringView AppBar::getTitle() const {
100 0 : return _label->getString8();
101 : }
102 :
103 10 : void AppBar::setNavButtonIcon(IconName name, float progress) {
104 10 : _navButton->setLeadingIconName(name, progress);
105 10 : _contentSizeDirty = true;
106 10 : }
107 :
108 70 : IconName AppBar::getNavButtonIcon() const {
109 70 : return _navButton->getLeadingIconName();
110 : }
111 :
112 10 : void AppBar::setMaxActionIcons(size_t value) {
113 10 : _maxActionIcons = value;
114 10 : _contentSizeDirty = true;
115 10 : }
116 0 : size_t AppBar::getMaxActionIcons() const {
117 0 : return _maxActionIcons;
118 : }
119 :
120 10 : void AppBar::setActionMenuSource(MenuSource *source) {
121 10 : if (_actionMenuSourceListener->getSubscription() != source) {
122 10 : _actionMenuSourceListener->setSubscription(source);
123 : }
124 10 : }
125 :
126 0 : void AppBar::replaceActionMenuSource(MenuSource *source, size_t maxIcons) {
127 0 : if (source == _actionMenuSourceListener->getSubscription()) {
128 0 : return;
129 : }
130 :
131 0 : if (maxIcons == maxOf<size_t>()) {
132 0 : maxIcons = source->getHintCount();
133 : }
134 :
135 0 : stopAllActionsByTag("replaceActionMenuSource"_tag);
136 0 : if (_prevComposer) {
137 0 : _prevComposer->removeFromParent();
138 0 : _prevComposer = nullptr;
139 : }
140 :
141 0 : _actionMenuSourceListener->setSubscription(source);
142 0 : _maxActionIcons = maxIcons;
143 :
144 0 : _prevComposer = _iconsComposer;
145 0 : float pos = -_prevComposer->getContentSize().height;
146 0 : _iconsComposer = _scissorNode->addChild(Rc<Node>::create(), ZOrder(1));
147 0 : _iconsComposer->setPosition(Vec2(0, pos));
148 0 : _iconsComposer->setAnchorPoint(Anchor::BottomLeft);
149 0 : _iconsComposer->setCascadeOpacityEnabled(true);
150 :
151 0 : float iconWidth = updateMenu(_iconsComposer, source, _maxActionIcons);
152 0 : if (iconWidth > _iconWidth) {
153 0 : _iconWidth = iconWidth;
154 0 : _contentSizeDirty = true;
155 : }
156 :
157 0 : _replaceProgress = 0.0f;
158 0 : updateProgress();
159 :
160 0 : runAction(Rc<ActionProgress>::create(0.15f, [this] (float p) {
161 0 : _replaceProgress = p;
162 0 : updateProgress();
163 0 : }, nullptr, [this] () {
164 0 : _replaceProgress = 1.0f;
165 0 : updateProgress();
166 0 : _contentSizeDirty = true;
167 0 : }), "replaceActionMenuSource"_tag);
168 : }
169 :
170 0 : MenuSource * AppBar::getActionMenuSource() const {
171 0 : return _actionMenuSourceListener->getSubscription();
172 : }
173 :
174 0 : void AppBar::setBasicHeight(float value) {
175 0 : if (isnan(value)) {
176 0 : _basicHeight = value;
177 0 : _contentSizeDirty = true;
178 : } else {
179 0 : if (_basicHeight != value) {
180 0 : _basicHeight = value;
181 0 : _contentSizeDirty = true;
182 : }
183 : }
184 0 : }
185 :
186 0 : float AppBar::getBasicHeight() const {
187 0 : return _basicHeight;
188 : }
189 :
190 10 : void AppBar::setNavCallback(Function<void()> &&cb) {
191 10 : _navCallback = move(cb);
192 10 : }
193 0 : const Function<void()> & AppBar::getNavCallback() const {
194 0 : return _navCallback;
195 : }
196 :
197 0 : void AppBar::setSwallowTouches(bool value) {
198 0 : if (value) {
199 0 : _inputListener->setSwallowEvents(InputListener::EventMaskTouch);
200 : } else {
201 0 : _inputListener->clearSwallowEvents(InputListener::EventMaskTouch);
202 : }
203 0 : }
204 :
205 0 : bool AppBar::isSwallowTouches() const {
206 0 : return _inputListener->isSwallowAllEvents(InputListener::EventMaskTouch);
207 : }
208 :
209 10 : Button *AppBar::getNavNode() const {
210 10 : return _navButton;
211 : }
212 :
213 0 : void AppBar::setBarCallback(Function<void()> &&cb) {
214 0 : _barCallback = move(cb);
215 0 : }
216 :
217 0 : const Function<void()> & AppBar::getBarCallback() const {
218 0 : return _barCallback;
219 : }
220 :
221 0 : void AppBar::handleNavTapped() {
222 0 : if (_navCallback) {
223 0 : _navCallback();
224 : }
225 0 : }
226 :
227 30 : void AppBar::updateProgress() {
228 30 : if (_replaceProgress == 1.0f) {
229 30 : if (_prevComposer) {
230 0 : _prevComposer->removeFromParent();
231 0 : _prevComposer = nullptr;
232 : }
233 : }
234 :
235 30 : if (_iconsComposer) {
236 30 : _iconsComposer->setPositionY(progress(_iconsComposer->getContentSize().height, 0.0f, _replaceProgress));
237 : }
238 30 : if (_prevComposer) {
239 0 : _prevComposer->setPositionY(progress(0.0f, -_prevComposer->getContentSize().height, _replaceProgress));
240 0 : _prevComposer->setOpacity(1.0f - _replaceProgress);
241 : }
242 30 : }
243 :
244 30 : float AppBar::updateMenu(Node *composer, MenuSource *source, size_t maxIcons) {
245 30 : composer->removeAllChildren();
246 30 : composer->setContentSize(_contentSize);
247 :
248 30 : float baseline = getBaseLine();
249 30 : size_t iconsCount = 0;
250 30 : auto extMenuSource = Rc<MenuSource>::create();
251 30 : Vector<Button *> icons;
252 30 : bool hasExtMenu = false;
253 :
254 30 : if (source) {
255 30 : auto &menuItems = source->getItems();
256 150 : for (auto &item : menuItems) {
257 120 : if (item->getType() == MenuSourceItem::Type::Button) {
258 120 : auto btnSrc = dynamic_cast<MenuSourceButton *>(item.get());
259 120 : if (btnSrc->getNameIcon() != IconName::None) {
260 120 : if (iconsCount < maxIcons) {
261 120 : auto btn = composer->addChild(Rc<Button>::create(NodeStyle::Text), ZOrder(0), iconsCount);
262 120 : btn->setMenuSourceButton(btnSrc);
263 120 : btn->setIconSize(24.0f);
264 120 : btn->setFollowContentSize(false);
265 120 : btn->setSwallowEvents(true);
266 120 : btn->setNodeMask(Button::LeadingIcon);
267 120 : icons.push_back(btn);
268 120 : iconsCount ++;
269 : } else {
270 0 : extMenuSource->addItem(item);
271 : }
272 : }
273 : }
274 : }
275 : }
276 :
277 30 : if (extMenuSource->count() > 0) {
278 0 : auto btn = Rc<Button>::create();
279 0 : auto source = Rc<MenuSourceButton>::create(StringView(), IconName::Navigation_more_vert_solid, move(extMenuSource));
280 0 : btn->setMenuSourceButton(move(source));
281 0 : btn->setFollowContentSize(false);
282 0 : btn->setSwallowEvents(true);
283 0 : btn->setNodeMask(Button::LeadingIcon);
284 0 : icons.push_back(btn);
285 0 : composer->addChild(move(btn), ZOrder(0), iconsCount);
286 0 : hasExtMenu = true;
287 0 : } else {
288 30 : hasExtMenu = false;
289 : }
290 :
291 30 : if (icons.size() > 0) {
292 30 : if (icons.back()->getLeadingIconName() == IconName::Navigation_more_vert_solid) {
293 0 : hasExtMenu = true;
294 : }
295 30 : auto pos = composer->getContentSize().width - 56 * (icons.size() - 1) - (hasExtMenu?8:36);
296 150 : for (auto &it : icons) {
297 120 : it->setContentSize(Size2(48, std::min(48.0f, getRealHeight())));
298 120 : it->setAnchorPoint(Vec2(0.5, 0.5));
299 120 : it->setPosition(Vec2(pos, baseline));
300 120 : pos += 56;
301 : }
302 30 : if (hasExtMenu) {
303 0 : icons.back()->setContentSize(Size2(24, std::min(48.0f, getRealHeight())));
304 0 : icons.back()->setPosition(Vec2(composer->getContentSize().width - 24, baseline));
305 : }
306 : }
307 :
308 30 : if (icons.size() > 0) {
309 30 : return (56 * (icons.size()) - (hasExtMenu?24:0));
310 : }
311 0 : return 0;
312 30 : }
313 :
314 30 : void AppBar::layoutSubviews() {
315 30 : _scissorNode->setContentSize(_contentSize);
316 :
317 30 : updateProgress();
318 :
319 30 : _iconsComposer->setContentSize(_scissorNode->getContentSize());
320 30 : auto iconWidth = updateMenu(_iconsComposer, _actionMenuSourceListener->getSubscription(), _maxActionIcons);
321 30 : if (_replaceProgress != 1.0f && _iconWidth != 0.0f) {
322 0 : _iconWidth = std::max(iconWidth, _iconWidth);
323 : } else {
324 30 : _iconWidth = iconWidth;
325 : }
326 :
327 30 : float baseline = getBaseLine();
328 30 : if (_navButton->getLeadingIconName() != IconName::Empty && _navButton->getLeadingIconName() != IconName::None) {
329 30 : _navButton->setContentSize(Size2(48.0f, 48.0f));
330 30 : _navButton->setAnchorPoint(Anchor::Middle);
331 30 : _navButton->setPosition(Vec2(32.0f, baseline));
332 30 : _navButton->setVisible(true);
333 : } else {
334 0 : _navButton->setVisible(false);
335 : }
336 :
337 30 : float labelStart = (getNavButtonIcon() == IconName::None) ? 16.0f : 64.0f;
338 30 : float labelEnd = _contentSize.width - _iconWidth - 16.0f;
339 :
340 30 : _label->setMaxWidth(labelEnd - labelStart);
341 :
342 30 : switch (_layout) {
343 0 : case AppBarLayout::CenterAligned:
344 0 : _label->setAnchorPoint(Anchor::Middle);
345 0 : _label->setAlignment(Label::TextAlign::Center);
346 0 : _label->setPosition(Vec2((labelStart + labelEnd) / 2.0f, baseline));
347 0 : break;
348 30 : case AppBarLayout::Small:
349 30 : _label->setAnchorPoint(Anchor::MiddleLeft);
350 30 : _label->setAlignment(Label::TextAlign::Left);
351 30 : _label->setPosition((getNavButtonIcon() == IconName::None) ? Vec2(16.0f, baseline) : Vec2(64.0f, baseline));
352 30 : break;
353 0 : case AppBarLayout::Minified:
354 0 : _label->setRole(TypescaleRole::TitleMedium);
355 0 : _label->setAnchorPoint(Anchor::MiddleLeft);
356 0 : _label->setAlignment(Label::TextAlign::Left);
357 0 : _label->setPosition((getNavButtonIcon() == IconName::None) ? Vec2(16.0f, baseline) : Vec2(64.0f, baseline));
358 0 : break;
359 0 : default:
360 0 : break;
361 : }
362 30 : }
363 :
364 60 : float AppBar::getBaseLine() const {
365 60 : auto v = _defaultHeight;
366 60 : if (!isnan(_basicHeight)) {
367 0 : v = std::max(v, _basicHeight);
368 : }
369 60 : if (_contentSize.height > _basicHeight) {
370 0 : return _contentSize.height - v / 2;
371 : } else {
372 60 : return v / 2;
373 : }
374 : }
375 :
376 150 : float AppBar::getRealHeight() const {
377 150 : auto v = _defaultHeight;
378 150 : if (!isnan(_basicHeight)) {
379 0 : v = std::max(v, _basicHeight);
380 : }
381 150 : return v;
382 : }
383 :
384 30 : void AppBar::updateDefaultHeight() {
385 30 : switch (_layout) {
386 30 : case AppBarLayout::CenterAligned:
387 : case AppBarLayout::Small:
388 : case AppBarLayout::Medium:
389 : case AppBarLayout::Large:
390 30 : _defaultHeight = 56.0f;
391 30 : break;
392 0 : case AppBarLayout::Minified:
393 0 : _defaultHeight = 32.0f;
394 0 : break;
395 : }
396 :
397 30 : _minHeight = 0.0f;
398 30 : _maxHeight = getRealHeight();
399 30 : }
400 :
401 : }
|