1 #include <algorithm>
2 #include <mutex>
3
4 #include "Common/Input/InputState.h"
5 #include "Common/Input/KeyCodes.h"
6 #include "Common/Render/DrawBuffer.h"
7 #include "Common/Render/TextureAtlas.h"
8 #include "Common/Data/Encoding/Utf8.h"
9 #include "Common/Data/Text/I18n.h"
10 #include "Common/UI/UI.h"
11 #include "Common/UI/View.h"
12 #include "Common/UI/Context.h"
13 #include "Common/UI/Tween.h"
14 #include "Common/UI/Root.h"
15 #include "Common/GPU/thin3d.h"
16 #include "Common/System/System.h"
17 #include "Common/TimeUtil.h"
18 #include "Common/StringUtils.h"
19
20 namespace UI {
21
22 const float ITEM_HEIGHT = 64.f;
23 const float MIN_TEXT_SCALE = 0.8f;
24 const float MAX_ITEM_SIZE = 65535.0f;
25
MeasureBySpec(Size sz,float contentWidth,MeasureSpec spec,float * measured)26 void MeasureBySpec(Size sz, float contentWidth, MeasureSpec spec, float *measured) {
27 *measured = sz;
28 if (sz == WRAP_CONTENT) {
29 if (spec.type == UNSPECIFIED)
30 *measured = contentWidth;
31 else if (spec.type == AT_MOST)
32 *measured = contentWidth < spec.size ? contentWidth : spec.size;
33 else if (spec.type == EXACTLY)
34 *measured = spec.size;
35 } else if (sz == FILL_PARENT) {
36 // UNSPECIFIED may have a minimum size of the parent. Let's use it to fill.
37 if (spec.type == UNSPECIFIED)
38 *measured = std::max(spec.size, contentWidth);
39 else
40 *measured = spec.size;
41 } else if (spec.type == EXACTLY || (spec.type == AT_MOST && *measured > spec.size)) {
42 *measured = spec.size;
43 }
44 }
45
ApplyBoundBySpec(float & bound,MeasureSpec spec)46 void ApplyBoundBySpec(float &bound, MeasureSpec spec) {
47 switch (spec.type) {
48 case AT_MOST:
49 bound = bound < spec.size ? bound : spec.size;
50 break;
51 case EXACTLY:
52 bound = spec.size;
53 break;
54 case UNSPECIFIED:
55 break;
56 }
57 }
58
ApplyBoundsBySpec(Bounds & bounds,MeasureSpec horiz,MeasureSpec vert)59 void ApplyBoundsBySpec(Bounds &bounds, MeasureSpec horiz, MeasureSpec vert) {
60 ApplyBoundBySpec(bounds.w, horiz);
61 ApplyBoundBySpec(bounds.h, vert);
62 }
63
Add(std::function<EventReturn (EventParams &)> func)64 void Event::Add(std::function<EventReturn(EventParams&)> func) {
65 HandlerRegistration reg;
66 reg.func = func;
67 handlers_.push_back(reg);
68 }
69
70 // Call this from input thread or whatever, it doesn't matter
Trigger(EventParams & e)71 void Event::Trigger(EventParams &e) {
72 EventTriggered(this, e);
73 }
74
75 // Call this from UI thread
Dispatch(EventParams & e)76 EventReturn Event::Dispatch(EventParams &e) {
77 for (auto iter = handlers_.begin(); iter != handlers_.end(); ++iter) {
78 if ((iter->func)(e) == UI::EVENT_DONE) {
79 // Event is handled, stop looping immediately. This event might even have gotten deleted.
80 return UI::EVENT_DONE;
81 }
82 }
83 return UI::EVENT_SKIPPED;
84 }
85
~Event()86 Event::~Event() {
87 handlers_.clear();
88 RemoveQueuedEventsByEvent(this);
89 }
90
~View()91 View::~View() {
92 if (HasFocus())
93 SetFocusedView(0);
94 RemoveQueuedEventsByView(this);
95
96 // Could use unique_ptr, but then we have to include tween everywhere.
97 for (auto &tween : tweens_)
98 delete tween;
99 tweens_.clear();
100 }
101
Update()102 void View::Update() {
103 for (size_t i = 0; i < tweens_.size(); ++i) {
104 Tween *tween = tweens_[i];
105 if (!tween->Finished()) {
106 tween->Apply(this);
107 } else if (!tween->Persists()) {
108 tweens_.erase(tweens_.begin() + i);
109 i--;
110 delete tween;
111 }
112 }
113 }
114
Measure(const UIContext & dc,MeasureSpec horiz,MeasureSpec vert)115 void View::Measure(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert) {
116 float contentW = 0.0f, contentH = 0.0f;
117 GetContentDimensionsBySpec(dc, horiz, vert, contentW, contentH);
118 MeasureBySpec(layoutParams_->width, contentW, horiz, &measuredWidth_);
119 MeasureBySpec(layoutParams_->height, contentH, vert, &measuredHeight_);
120 }
121
122 // Default values
123
GetContentDimensions(const UIContext & dc,float & w,float & h) const124 void View::GetContentDimensions(const UIContext &dc, float &w, float &h) const {
125 w = 10.0f;
126 h = 10.0f;
127 }
128
GetContentDimensionsBySpec(const UIContext & dc,MeasureSpec horiz,MeasureSpec vert,float & w,float & h) const129 void View::GetContentDimensionsBySpec(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert, float &w, float &h) const {
130 GetContentDimensions(dc, w, h);
131 }
132
Query(float x,float y,std::vector<View * > & list)133 void View::Query(float x, float y, std::vector<View *> &list) {
134 if (bounds_.Contains(x, y)) {
135 list.push_back(this);
136 }
137 }
138
DescribeLog() const139 std::string View::DescribeLog() const {
140 return StringFromFormat("%0.1f,%0.1f %0.1fx%0.1f", bounds_.x, bounds_.y, bounds_.w, bounds_.h);
141 }
142
143
PersistData(PersistStatus status,std::string anonId,PersistMap & storage)144 void View::PersistData(PersistStatus status, std::string anonId, PersistMap &storage) {
145 // Remember if this view was a focused view.
146 std::string tag = Tag();
147 if (tag.empty()) {
148 tag = anonId;
149 }
150
151 const std::string focusedKey = "ViewFocused::" + tag;
152 switch (status) {
153 case UI::PERSIST_SAVE:
154 if (HasFocus()) {
155 storage[focusedKey].resize(1);
156 }
157 break;
158 case UI::PERSIST_RESTORE:
159 if (storage.find(focusedKey) != storage.end()) {
160 SetFocus();
161 }
162 break;
163 }
164
165 for (int i = 0; i < (int)tweens_.size(); ++i) {
166 tweens_[i]->PersistData(status, tag + "/" + StringFromInt(i), storage);
167 }
168 }
169
GetFocusPosition(FocusDirection dir)170 Point View::GetFocusPosition(FocusDirection dir) {
171 // The +2/-2 is some extra fudge factor to cover for views sitting right next to each other.
172 // Distance zero yields strange results otherwise.
173 switch (dir) {
174 case FOCUS_LEFT: return Point(bounds_.x + 2, bounds_.centerY());
175 case FOCUS_RIGHT: return Point(bounds_.x2() - 2, bounds_.centerY());
176 case FOCUS_UP: return Point(bounds_.centerX(), bounds_.y + 2);
177 case FOCUS_DOWN: return Point(bounds_.centerX(), bounds_.y2() - 2);
178
179 default:
180 return bounds_.Center();
181 }
182 }
183
SetFocus()184 bool View::SetFocus() {
185 if (IsFocusMovementEnabled()) {
186 if (CanBeFocused()) {
187 SetFocusedView(this);
188 return true;
189 }
190 }
191 return false;
192 }
193
Clickable(LayoutParams * layoutParams)194 Clickable::Clickable(LayoutParams *layoutParams)
195 : View(layoutParams) {
196 // We set the colors later once we have a UIContext.
197 bgColor_ = AddTween(new CallbackColorTween(0.1f));
198 bgColor_->Persist();
199 }
200
DrawBG(UIContext & dc,const Style & style)201 void Clickable::DrawBG(UIContext &dc, const Style &style) {
202 if (style.background.type == DRAW_SOLID_COLOR) {
203 if (time_now_d() - bgColorLast_ >= 0.25f) {
204 bgColor_->Reset(style.background.color);
205 } else if (bgColor_->CurrentValue() != style.background.color) {
206 bgColor_->Divert(style.background.color, down_ ? 0.05f : 0.1f);
207 }
208 bgColorLast_ = time_now_d();
209
210 dc.FillRect(Drawable(bgColor_->CurrentValue()), bounds_);
211 } else {
212 dc.FillRect(style.background, bounds_);
213 }
214 }
215
Click()216 void Clickable::Click() {
217 UI::EventParams e{};
218 e.v = this;
219 OnClick.Trigger(e);
220 };
221
FocusChanged(int focusFlags)222 void Clickable::FocusChanged(int focusFlags) {
223 if (focusFlags & FF_LOSTFOCUS) {
224 down_ = false;
225 dragging_ = false;
226 }
227 }
228
Touch(const TouchInput & input)229 void Clickable::Touch(const TouchInput &input) {
230 if (!IsEnabled()) {
231 dragging_ = false;
232 down_ = false;
233 return;
234 }
235
236 if (input.flags & TOUCH_DOWN) {
237 if (bounds_.Contains(input.x, input.y)) {
238 if (IsFocusMovementEnabled())
239 SetFocusedView(this);
240 dragging_ = true;
241 down_ = true;
242 } else {
243 down_ = false;
244 dragging_ = false;
245 }
246 } else if (input.flags & TOUCH_MOVE) {
247 if (dragging_)
248 down_ = bounds_.Contains(input.x, input.y);
249 }
250 if (input.flags & TOUCH_UP) {
251 if ((input.flags & TOUCH_CANCEL) == 0 && dragging_ && bounds_.Contains(input.x, input.y)) {
252 Click();
253 }
254 down_ = false;
255 downCountDown_ = 0;
256 dragging_ = false;
257 }
258 }
259
MatchesKeyDef(const std::vector<KeyDef> & defs,const KeyInput & key)260 static bool MatchesKeyDef(const std::vector<KeyDef> &defs, const KeyInput &key) {
261 // In addition to the actual search, we need to do another search where we replace the device ID with "ANY".
262 return
263 std::find(defs.begin(), defs.end(), KeyDef(key.deviceId, key.keyCode)) != defs.end() ||
264 std::find(defs.begin(), defs.end(), KeyDef(DEVICE_ID_ANY, key.keyCode)) != defs.end();
265 }
266
267 // TODO: O/X confirm preference for xperia play?
268
IsDPadKey(const KeyInput & key)269 bool IsDPadKey(const KeyInput &key) {
270 if (dpadKeys.empty()) {
271 return key.keyCode >= NKCODE_DPAD_UP && key.keyCode <= NKCODE_DPAD_RIGHT;
272 } else {
273 return MatchesKeyDef(dpadKeys, key);
274 }
275 }
276
IsAcceptKey(const KeyInput & key)277 bool IsAcceptKey(const KeyInput &key) {
278 if (confirmKeys.empty()) {
279 // This path is pretty much not used, confirmKeys should be set.
280 // TODO: Get rid of this stuff?
281 if (key.deviceId == DEVICE_ID_KEYBOARD) {
282 return key.keyCode == NKCODE_SPACE || key.keyCode == NKCODE_ENTER || key.keyCode == NKCODE_Z;
283 } else {
284 return key.keyCode == NKCODE_BUTTON_A || key.keyCode == NKCODE_BUTTON_CROSS || key.keyCode == NKCODE_BUTTON_1 || key.keyCode == NKCODE_DPAD_CENTER;
285 }
286 } else {
287 return MatchesKeyDef(confirmKeys, key);
288 }
289 }
290
IsEscapeKey(const KeyInput & key)291 bool IsEscapeKey(const KeyInput &key) {
292 if (cancelKeys.empty()) {
293 // This path is pretty much not used, cancelKeys should be set.
294 // TODO: Get rid of this stuff?
295 if (key.deviceId == DEVICE_ID_KEYBOARD) {
296 return key.keyCode == NKCODE_ESCAPE || key.keyCode == NKCODE_BACK;
297 } else {
298 return key.keyCode == NKCODE_BUTTON_CIRCLE || key.keyCode == NKCODE_BUTTON_B || key.keyCode == NKCODE_BUTTON_2;
299 }
300 } else {
301 return MatchesKeyDef(cancelKeys, key);
302 }
303 }
304
IsTabLeftKey(const KeyInput & key)305 bool IsTabLeftKey(const KeyInput &key) {
306 if (tabLeftKeys.empty()) {
307 // This path is pretty much not used, tabLeftKeys should be set.
308 // TODO: Get rid of this stuff?
309 return key.keyCode == NKCODE_BUTTON_L1;
310 } else {
311 return MatchesKeyDef(tabLeftKeys, key);
312 }
313 }
314
IsTabRightKey(const KeyInput & key)315 bool IsTabRightKey(const KeyInput &key) {
316 if (tabRightKeys.empty()) {
317 // This path is pretty much not used, tabRightKeys should be set.
318 // TODO: Get rid of this stuff?
319 return key.keyCode == NKCODE_BUTTON_R1;
320 } else {
321 return MatchesKeyDef(tabRightKeys, key);
322 }
323 }
324
Key(const KeyInput & key)325 bool Clickable::Key(const KeyInput &key) {
326 if (!HasFocus() && key.deviceId != DEVICE_ID_MOUSE) {
327 down_ = false;
328 return false;
329 }
330 // TODO: Replace most of Update with this.
331
332 bool ret = false;
333 if (key.flags & KEY_DOWN) {
334 if (IsAcceptKey(key)) {
335 down_ = true;
336 ret = true;
337 }
338 }
339 if (key.flags & KEY_UP) {
340 if (IsAcceptKey(key)) {
341 if (down_) {
342 Click();
343 down_ = false;
344 ret = true;
345 }
346 } else if (IsEscapeKey(key)) {
347 down_ = false;
348 }
349 }
350 return ret;
351 }
352
Touch(const TouchInput & input)353 void StickyChoice::Touch(const TouchInput &input) {
354 dragging_ = false;
355 if (!IsEnabled()) {
356 down_ = false;
357 return;
358 }
359
360 if (input.flags & TOUCH_DOWN) {
361 if (bounds_.Contains(input.x, input.y)) {
362 if (IsFocusMovementEnabled())
363 SetFocusedView(this);
364 down_ = true;
365 Click();
366 }
367 }
368 }
369
Key(const KeyInput & key)370 bool StickyChoice::Key(const KeyInput &key) {
371 if (!HasFocus()) {
372 return false;
373 }
374
375 // TODO: Replace most of Update with this.
376 if (key.flags & KEY_DOWN) {
377 if (IsAcceptKey(key)) {
378 down_ = true;
379 UI::PlayUISound(UI::UISound::TOGGLE_ON);
380 Click();
381 return true;
382 }
383 }
384 return false;
385 }
386
FocusChanged(int focusFlags)387 void StickyChoice::FocusChanged(int focusFlags) {
388 // Override Clickable's FocusChanged to do nothing.
389 }
390
Item(LayoutParams * layoutParams)391 Item::Item(LayoutParams *layoutParams) : InertView(layoutParams) {
392 if (!layoutParams) {
393 layoutParams_->width = FILL_PARENT;
394 layoutParams_->height = ITEM_HEIGHT;
395 }
396 }
397
GetContentDimensions(const UIContext & dc,float & w,float & h) const398 void Item::GetContentDimensions(const UIContext &dc, float &w, float &h) const {
399 w = 0.0f;
400 h = 0.0f;
401 }
402
GetContentDimensions(const UIContext & dc,float & w,float & h) const403 void ClickableItem::GetContentDimensions(const UIContext &dc, float &w, float &h) const {
404 w = 0.0f;
405 h = ITEM_HEIGHT;
406 }
407
ClickableItem(LayoutParams * layoutParams)408 ClickableItem::ClickableItem(LayoutParams *layoutParams) : Clickable(layoutParams) {
409 if (!layoutParams) {
410 if (layoutParams_->width == WRAP_CONTENT)
411 layoutParams_->width = FILL_PARENT;
412 }
413 }
414
Draw(UIContext & dc)415 void ClickableItem::Draw(UIContext &dc) {
416 Style style = dc.theme->itemStyle;
417
418 if (HasFocus()) {
419 style = dc.theme->itemFocusedStyle;
420 }
421 if (down_) {
422 style = dc.theme->itemDownStyle;
423 }
424
425 DrawBG(dc, style);
426 }
427
Click()428 void Choice::Click() {
429 ClickableItem::Click();
430 UI::PlayUISound(UI::UISound::CONFIRM);
431 }
432
GetContentDimensionsBySpec(const UIContext & dc,MeasureSpec horiz,MeasureSpec vert,float & w,float & h) const433 void Choice::GetContentDimensionsBySpec(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert, float &w, float &h) const {
434 float totalW = 0.0f;
435 float totalH = 0.0f;
436 if (image_.isValid()) {
437 dc.Draw()->GetAtlas()->measureImage(image_, &w, &h);
438 totalW = w + 6;
439 totalH = h;
440 }
441 if (!text_.empty()) {
442 const int paddingX = 12;
443 float availWidth = horiz.size - paddingX * 2 - textPadding_.horiz() - totalW;
444 if (availWidth < 0.0f) {
445 // Let it have as much space as it needs.
446 availWidth = MAX_ITEM_SIZE;
447 }
448 if (horiz.type != EXACTLY && layoutParams_->width > 0.0f && availWidth > layoutParams_->width)
449 availWidth = layoutParams_->width;
450 float scale = CalculateTextScale(dc, availWidth);
451 Bounds availBounds(0, 0, availWidth, vert.size);
452 float textW = 0.0f, textH = 0.0f;
453 dc.MeasureTextRect(dc.theme->uiFont, scale, scale, text_.c_str(), (int)text_.size(), availBounds, &textW, &textH, FLAG_WRAP_TEXT);
454 totalH = std::max(totalH, textH);
455 totalW += textW;
456 }
457
458 w = totalW + 24;
459 h = totalH + 16;
460 h = std::max(h, ITEM_HEIGHT);
461 }
462
CalculateTextScale(const UIContext & dc,float availWidth) const463 float Choice::CalculateTextScale(const UIContext &dc, float availWidth) const {
464 float actualWidth, actualHeight;
465 Bounds availBounds(0, 0, availWidth, bounds_.h);
466 dc.MeasureTextRect(dc.theme->uiFont, 1.0f, 1.0f, text_.c_str(), (int)text_.size(), availBounds, &actualWidth, &actualHeight);
467 if (actualWidth > availWidth) {
468 return std::max(MIN_TEXT_SCALE, availWidth / actualWidth);
469 }
470 return 1.0f;
471 }
472
HighlightChanged(bool highlighted)473 void Choice::HighlightChanged(bool highlighted){
474 highlighted_ = highlighted;
475 }
476
Draw(UIContext & dc)477 void Choice::Draw(UIContext &dc) {
478 if (!IsSticky()) {
479 ClickableItem::Draw(dc);
480 } else {
481 Style style = dc.theme->itemStyle;
482 if (highlighted_) {
483 style = dc.theme->itemHighlightedStyle;
484 }
485 if (down_) {
486 style = dc.theme->itemDownStyle;
487 }
488 if (HasFocus()) {
489 style = dc.theme->itemFocusedStyle;
490 }
491
492 DrawBG(dc, style);
493 }
494
495 Style style = dc.theme->itemStyle;
496 if (!IsEnabled()) {
497 style = dc.theme->itemDisabledStyle;
498 }
499
500 if (image_.isValid() && text_.empty()) {
501 dc.Draw()->DrawImageRotated(image_, bounds_.centerX(), bounds_.centerY(), imgScale_, imgRot_, style.fgColor, imgFlipH_);
502 } else {
503 dc.SetFontStyle(dc.theme->uiFont);
504
505 int paddingX = 12;
506 float availWidth = bounds_.w - paddingX * 2 - textPadding_.horiz();
507
508 if (image_.isValid()) {
509 const AtlasImage *image = dc.Draw()->GetAtlas()->getImage(image_);
510 paddingX += image->w + 6;
511 availWidth -= image->w + 6;
512 // TODO: Use scale rotation and flip here as well (DrawImageRotated is always ALIGN_CENTER for now)
513 dc.Draw()->DrawImage(image_, bounds_.x + 6, bounds_.centerY(), 1.0f, 0xFFFFFFFF, ALIGN_LEFT | ALIGN_VCENTER);
514 }
515
516 float scale = CalculateTextScale(dc, availWidth);
517
518 dc.SetFontScale(scale, scale);
519 if (centered_) {
520 dc.DrawTextRect(text_.c_str(), bounds_, style.fgColor, ALIGN_CENTER | FLAG_WRAP_TEXT);
521 } else {
522 if (rightIconImage_.isValid()) {
523 dc.Draw()->DrawImageRotated(rightIconImage_, bounds_.x2() - 32 - paddingX, bounds_.centerY(), rightIconScale_, rightIconRot_, style.fgColor, rightIconFlipH_);
524 }
525 Bounds textBounds(bounds_.x + paddingX + textPadding_.left, bounds_.y, availWidth, bounds_.h);
526 dc.DrawTextRect(text_.c_str(), textBounds, style.fgColor, ALIGN_VCENTER | FLAG_WRAP_TEXT);
527 }
528 dc.SetFontScale(1.0f, 1.0f);
529 }
530
531 if (selected_) {
532 dc.Draw()->DrawImage(dc.theme->checkOn, bounds_.x2() - 40, bounds_.centerY(), 1.0f, style.fgColor, ALIGN_CENTER);
533 }
534 }
535
DescribeText() const536 std::string Choice::DescribeText() const {
537 auto u = GetI18NCategory("UI Elements");
538 return ReplaceAll(u->T("%1 choice"), "%1", text_);
539 }
540
InfoItem(const std::string & text,const std::string & rightText,LayoutParams * layoutParams)541 InfoItem::InfoItem(const std::string &text, const std::string &rightText, LayoutParams *layoutParams)
542 : Item(layoutParams), text_(text), rightText_(rightText) {
543 // We set the colors later once we have a UIContext.
544 bgColor_ = AddTween(new CallbackColorTween(0.1f));
545 bgColor_->Persist();
546 fgColor_ = AddTween(new CallbackColorTween(0.1f));
547 fgColor_->Persist();
548 }
549
Draw(UIContext & dc)550 void InfoItem::Draw(UIContext &dc) {
551 Item::Draw(dc);
552
553 UI::Style style = HasFocus() ? dc.theme->itemFocusedStyle : dc.theme->infoStyle;
554
555 if (choiceStyle_) {
556 style = HasFocus() ? dc.theme->buttonFocusedStyle : dc.theme->buttonStyle;
557 }
558
559
560 if (style.background.type == DRAW_SOLID_COLOR) {
561 // For a smoother fade, using the same color with 0 alpha.
562 if ((style.background.color & 0xFF000000) == 0)
563 style.background.color = dc.theme->itemFocusedStyle.background.color & 0x00FFFFFF;
564 bgColor_->Divert(style.background.color & 0x7fffffff);
565 style.background.color = bgColor_->CurrentValue();
566 }
567 fgColor_->Divert(style.fgColor);
568 style.fgColor = fgColor_->CurrentValue();
569
570 dc.FillRect(style.background, bounds_);
571
572 int paddingX = 12;
573 Bounds padBounds = bounds_.Expand(-paddingX, 0);
574
575 float leftWidth, leftHeight;
576 dc.MeasureTextRect(dc.theme->uiFont, 1.0f, 1.0f, text_.c_str(), (int)text_.size(), padBounds, &leftWidth, &leftHeight, ALIGN_VCENTER);
577
578 dc.SetFontStyle(dc.theme->uiFont);
579 dc.DrawTextRect(text_.c_str(), padBounds, style.fgColor, ALIGN_VCENTER);
580
581 Bounds rightBounds(padBounds.x + leftWidth, padBounds.y, padBounds.w - leftWidth, padBounds.h);
582 dc.DrawTextRect(rightText_.c_str(), rightBounds, style.fgColor, ALIGN_VCENTER | ALIGN_RIGHT | FLAG_WRAP_TEXT);
583 }
584
DescribeText() const585 std::string InfoItem::DescribeText() const {
586 auto u = GetI18NCategory("UI Elements");
587 return ReplaceAll(ReplaceAll(u->T("%1: %2"), "%1", text_), "%2", rightText_);
588 }
589
ItemHeader(const std::string & text,LayoutParams * layoutParams)590 ItemHeader::ItemHeader(const std::string &text, LayoutParams *layoutParams)
591 : Item(layoutParams), text_(text) {
592 layoutParams_->width = FILL_PARENT;
593 layoutParams_->height = 40;
594 }
595
Draw(UIContext & dc)596 void ItemHeader::Draw(UIContext &dc) {
597 dc.SetFontStyle(dc.theme->uiFontSmall);
598 dc.DrawText(text_.c_str(), bounds_.x + 4, bounds_.centerY(), dc.theme->headerStyle.fgColor, ALIGN_LEFT | ALIGN_VCENTER);
599 dc.Draw()->DrawImageCenterTexel(dc.theme->whiteImage, bounds_.x, bounds_.y2()-2, bounds_.x2(), bounds_.y2(), dc.theme->headerStyle.fgColor);
600 }
601
GetContentDimensionsBySpec(const UIContext & dc,MeasureSpec horiz,MeasureSpec vert,float & w,float & h) const602 void ItemHeader::GetContentDimensionsBySpec(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert, float &w, float &h) const {
603 Bounds bounds(0, 0, layoutParams_->width, layoutParams_->height);
604 if (bounds.w < 0) {
605 // If there's no size, let's grow as big as we want.
606 bounds.w = horiz.size == 0 ? MAX_ITEM_SIZE : horiz.size;
607 }
608 if (bounds.h < 0) {
609 bounds.h = vert.size == 0 ? MAX_ITEM_SIZE : vert.size;
610 }
611 ApplyBoundsBySpec(bounds, horiz, vert);
612 dc.MeasureTextRect(dc.theme->uiFontSmall, 1.0f, 1.0f, text_.c_str(), (int)text_.length(), bounds, &w, &h, ALIGN_LEFT | ALIGN_VCENTER);
613 }
614
DescribeText() const615 std::string ItemHeader::DescribeText() const {
616 auto u = GetI18NCategory("UI Elements");
617 return ReplaceAll(u->T("%1 heading"), "%1", text_);
618 }
619
Draw(UIContext & dc)620 void BorderView::Draw(UIContext &dc) {
621 Color color = 0xFFFFFFFF;
622 if (style_ == BorderStyle::HEADER_FG)
623 color = dc.theme->headerStyle.fgColor;
624 else if (style_ == BorderStyle::ITEM_DOWN_BG)
625 color = dc.theme->itemDownStyle.background.color;
626
627 if (borderFlags_ & BORDER_TOP)
628 dc.Draw()->DrawImageCenterTexel(dc.theme->whiteImage, bounds_.x, bounds_.y, bounds_.x2(), bounds_.y + size_, color);
629 if (borderFlags_ & BORDER_LEFT)
630 dc.Draw()->DrawImageCenterTexel(dc.theme->whiteImage, bounds_.x, bounds_.y, bounds_.x + size_, bounds_.y2(), color);
631 if (borderFlags_ & BORDER_BOTTOM)
632 dc.Draw()->DrawImageCenterTexel(dc.theme->whiteImage, bounds_.x, bounds_.y2() - size_, bounds_.x2(), bounds_.y2(), color);
633 if (borderFlags_ & BORDER_RIGHT)
634 dc.Draw()->DrawImageCenterTexel(dc.theme->whiteImage, bounds_.x2() - size_, bounds_.y, bounds_.x2(), bounds_.y2(), color);
635 }
636
GetContentDimensionsBySpec(const UIContext & dc,MeasureSpec horiz,MeasureSpec vert,float & w,float & h) const637 void BorderView::GetContentDimensionsBySpec(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert, float &w, float &h) const {
638 Bounds bounds(0, 0, layoutParams_->width, layoutParams_->height);
639 if (bounds.w < 0) {
640 // If there's no size, let's grow as big as we want.
641 bounds.w = horiz.size == 0 ? MAX_ITEM_SIZE : horiz.size;
642 }
643 if (bounds.h < 0) {
644 bounds.h = vert.size == 0 ? MAX_ITEM_SIZE : vert.size;
645 }
646 ApplyBoundsBySpec(bounds, horiz, vert);
647 // If we have vertical borders, grow to width so they're spaced apart.
648 w = (borderFlags_ & BORDER_VERT) != 0 ? bounds.w : 0;
649 h = (borderFlags_ & BORDER_HORIZ) != 0 ? bounds.h : 0;
650 }
651
Draw(UIContext & dc)652 void PopupHeader::Draw(UIContext &dc) {
653 const float paddingHorizontal = 12;
654 const float availableWidth = bounds_.w - paddingHorizontal * 2;
655
656 float tw, th;
657 dc.SetFontStyle(dc.theme->uiFont);
658 dc.MeasureText(dc.GetFontStyle(), 1.0f, 1.0f, text_.c_str(), &tw, &th, 0);
659
660 float sineWidth = std::max(0.0f, (tw - availableWidth)) / 2.0f;
661
662 float tx = paddingHorizontal;
663 if (availableWidth < tw) {
664 float overageRatio = 1.5f * availableWidth * 1.0f / tw;
665 tx -= (1.0f + sin(time_now_d() * overageRatio)) * sineWidth;
666 Bounds tb = bounds_;
667 tb.x = bounds_.x + paddingHorizontal;
668 tb.w = bounds_.w - paddingHorizontal * 2;
669 dc.PushScissor(tb);
670 }
671
672 dc.DrawText(text_.c_str(), bounds_.x + tx, bounds_.centerY(), dc.theme->popupTitle.fgColor, ALIGN_LEFT | ALIGN_VCENTER);
673 dc.Draw()->DrawImageCenterTexel(dc.theme->whiteImage, bounds_.x, bounds_.y2()-2, bounds_.x2(), bounds_.y2(), dc.theme->popupTitle.fgColor);
674
675 if (availableWidth < tw) {
676 dc.PopScissor();
677 }
678 }
679
DescribeText() const680 std::string PopupHeader::DescribeText() const {
681 auto u = GetI18NCategory("UI Elements");
682 return ReplaceAll(u->T("%1 heading"), "%1", text_);
683 }
684
Toggle()685 void CheckBox::Toggle() {
686 if (toggle_) {
687 *toggle_ = !(*toggle_);
688 UI::PlayUISound(*toggle_ ? UI::UISound::TOGGLE_ON : UI::UISound::TOGGLE_OFF);
689 }
690 }
691
Toggled() const692 bool CheckBox::Toggled() const {
693 if (toggle_)
694 return *toggle_;
695 return false;
696 }
697
OnClicked(EventParams & e)698 EventReturn CheckBox::OnClicked(EventParams &e) {
699 Toggle();
700 return EVENT_CONTINUE; // It's safe to keep processing events.
701 }
702
Draw(UIContext & dc)703 void CheckBox::Draw(UIContext &dc) {
704 Style style = dc.theme->itemStyle;
705 if (!IsEnabled())
706 style = dc.theme->itemDisabledStyle;
707 dc.SetFontStyle(dc.theme->uiFont);
708
709 ClickableItem::Draw(dc);
710
711 ImageID image = Toggled() ? dc.theme->checkOn : dc.theme->checkOff;
712 float imageW, imageH;
713 dc.Draw()->MeasureImage(image, &imageW, &imageH);
714
715 const int paddingX = 12;
716 // Padding right of the checkbox image too.
717 const float availWidth = bounds_.w - paddingX * 2 - imageW - paddingX;
718 float scale = CalculateTextScale(dc, availWidth);
719
720 dc.SetFontScale(scale, scale);
721 Bounds textBounds(bounds_.x + paddingX, bounds_.y, availWidth, bounds_.h);
722 dc.DrawTextRect(text_.c_str(), textBounds, style.fgColor, ALIGN_VCENTER | FLAG_WRAP_TEXT);
723 dc.Draw()->DrawImage(image, bounds_.x2() - paddingX, bounds_.centerY(), 1.0f, style.fgColor, ALIGN_RIGHT | ALIGN_VCENTER);
724 dc.SetFontScale(1.0f, 1.0f);
725 }
726
DescribeText() const727 std::string CheckBox::DescribeText() const {
728 auto u = GetI18NCategory("UI Elements");
729 std::string text = ReplaceAll(u->T("%1 checkbox"), "%1", text_);
730 if (!smallText_.empty()) {
731 text += "\n" + smallText_;
732 }
733 return text;
734 }
735
CalculateTextScale(const UIContext & dc,float availWidth) const736 float CheckBox::CalculateTextScale(const UIContext &dc, float availWidth) const {
737 float actualWidth, actualHeight;
738 Bounds availBounds(0, 0, availWidth, bounds_.h);
739 dc.MeasureTextRect(dc.theme->uiFont, 1.0f, 1.0f, text_.c_str(), (int)text_.size(), availBounds, &actualWidth, &actualHeight, ALIGN_VCENTER);
740 if (actualWidth > availWidth) {
741 return std::max(MIN_TEXT_SCALE, availWidth / actualWidth);
742 }
743 return 1.0f;
744 }
745
GetContentDimensions(const UIContext & dc,float & w,float & h) const746 void CheckBox::GetContentDimensions(const UIContext &dc, float &w, float &h) const {
747 ImageID image = Toggled() ? dc.theme->checkOn : dc.theme->checkOff;
748 float imageW, imageH;
749 dc.Draw()->MeasureImage(image, &imageW, &imageH);
750
751 const int paddingX = 12;
752 // Padding right of the checkbox image too.
753 float availWidth = bounds_.w - paddingX * 2 - imageW - paddingX;
754 if (availWidth < 0.0f) {
755 // Let it have as much space as it needs.
756 availWidth = MAX_ITEM_SIZE;
757 }
758 float scale = CalculateTextScale(dc, availWidth);
759
760 float actualWidth, actualHeight;
761 Bounds availBounds(0, 0, availWidth, bounds_.h);
762 dc.MeasureTextRect(dc.theme->uiFont, scale, scale, text_.c_str(), (int)text_.size(), availBounds, &actualWidth, &actualHeight, ALIGN_VCENTER | FLAG_WRAP_TEXT);
763
764 w = bounds_.w;
765 h = std::max(actualHeight, ITEM_HEIGHT);
766 }
767
Toggle()768 void BitCheckBox::Toggle() {
769 if (bitfield_) {
770 *bitfield_ = *bitfield_ ^ bit_;
771 if (*bitfield_ & bit_) {
772 UI::PlayUISound(UI::UISound::TOGGLE_ON);
773 } else {
774 UI::PlayUISound(UI::UISound::TOGGLE_OFF);
775 }
776 }
777 }
778
Toggled() const779 bool BitCheckBox::Toggled() const {
780 if (bitfield_)
781 return (bit_ & *bitfield_) == bit_;
782 return false;
783 }
784
GetContentDimensions(const UIContext & dc,float & w,float & h) const785 void Button::GetContentDimensions(const UIContext &dc, float &w, float &h) const {
786 if (imageID_.isValid()) {
787 dc.Draw()->GetAtlas()->measureImage(imageID_, &w, &h);
788 } else {
789 w = 0.0f;
790 h = 0.0f;
791 }
792
793 if (!text_.empty() && !ignoreText_) {
794 float width = 0.0f;
795 float height = 0.0f;
796 dc.MeasureText(dc.theme->uiFont, 1.0f, 1.0f, text_.c_str(), &width, &height);
797
798 w += width;
799 if (imageID_.isValid()) {
800 w += paddingW_;
801 }
802 h = std::max(h, height);
803 }
804
805 // Add some internal padding to not look totally ugly
806 w += paddingW_;
807 h += paddingH_;
808
809 w *= scale_;
810 h *= scale_;
811 }
812
DescribeText() const813 std::string Button::DescribeText() const {
814 auto u = GetI18NCategory("UI Elements");
815 return ReplaceAll(u->T("%1 button"), "%1", GetText());
816 }
817
Click()818 void Button::Click() {
819 Clickable::Click();
820 UI::PlayUISound(UI::UISound::CONFIRM);
821 }
822
Draw(UIContext & dc)823 void Button::Draw(UIContext &dc) {
824 Style style = dc.theme->buttonStyle;
825
826 if (HasFocus()) style = dc.theme->buttonFocusedStyle;
827 if (down_) style = dc.theme->buttonDownStyle;
828 if (!IsEnabled()) style = dc.theme->buttonDisabledStyle;
829
830 // dc.Draw()->DrawImage4Grid(style.image, bounds_.x, bounds_.y, bounds_.x2(), bounds_.y2(), style.bgColor);
831 DrawBG(dc, style);
832 float tw, th;
833 dc.MeasureText(dc.theme->uiFont, 1.0f, 1.0f, text_.c_str(), &tw, &th);
834 tw *= scale_;
835 th *= scale_;
836
837 if (tw > bounds_.w || imageID_.isValid()) {
838 dc.PushScissor(bounds_);
839 }
840 dc.SetFontStyle(dc.theme->uiFont);
841 dc.SetFontScale(scale_, scale_);
842 if (imageID_.isValid() && (ignoreText_ || text_.empty())) {
843 dc.Draw()->DrawImage(imageID_, bounds_.centerX(), bounds_.centerY(), scale_, 0xFFFFFFFF, ALIGN_CENTER);
844 } else if (!text_.empty()) {
845 float textX = bounds_.centerX();
846 if (imageID_.isValid()) {
847 const AtlasImage *img = dc.Draw()->GetAtlas()->getImage(imageID_);
848 if (img) {
849 dc.Draw()->DrawImage(imageID_, bounds_.centerX() - tw / 2 - 5, bounds_.centerY(), 1.0f, 0xFFFFFFFF, ALIGN_CENTER);
850 textX += img->w / 2.0f;
851 }
852 }
853 dc.DrawText(text_.c_str(), textX, bounds_.centerY(), style.fgColor, ALIGN_CENTER);
854 }
855 dc.SetFontScale(1.0f, 1.0f);
856
857 if (tw > bounds_.w || imageID_.isValid()) {
858 dc.PopScissor();
859 }
860 }
861
GetContentDimensions(const UIContext & dc,float & w,float & h) const862 void RadioButton::GetContentDimensions(const UIContext &dc, float &w, float &h) const {
863 w = 0.0f;
864 h = 0.0f;
865
866 if (!text_.empty()) {
867 dc.MeasureText(dc.theme->uiFont, 1.0f, 1.0f, text_.c_str(), &w, &h);
868 }
869
870 // Add some internal padding to not look totally ugly
871 w += paddingW_ * 3.0f + radioRadius_ * 2.0f;
872 h = std::max(h, radioRadius_ * 2) + paddingH_ * 2;
873 }
874
DescribeText() const875 std::string RadioButton::DescribeText() const {
876 auto u = GetI18NCategory("UI Elements");
877 return ReplaceAll(u->T("%1 radio button"), "%1", text_);
878 }
879
Click()880 void RadioButton::Click() {
881 Clickable::Click();
882 UI::PlayUISound(UI::UISound::CONFIRM);
883 *value_ = thisButtonValue_;
884 }
885
Draw(UIContext & dc)886 void RadioButton::Draw(UIContext &dc) {
887 Style style = dc.theme->buttonStyle;
888
889 bool checked = *value_ == thisButtonValue_;
890
891 if (HasFocus()) style = dc.theme->buttonFocusedStyle;
892 if (down_) style = dc.theme->buttonDownStyle;
893 if (!IsEnabled()) style = dc.theme->buttonDisabledStyle;
894
895 DrawBG(dc, style);
896
897 dc.Flush();
898 dc.BeginNoTex();
899 dc.Draw()->Circle(bounds_.x + paddingW_ + radioRadius_, bounds_.centerY(), radioRadius_, 2.5f, 36, 0, style.fgColor, 1.0f);
900 if (checked) {
901 dc.Draw()->FillCircle(bounds_.x + paddingW_ + radioRadius_, bounds_.centerY(), radioInnerRadius_, 36, style.fgColor);
902 }
903 dc.Flush();
904 dc.Begin();
905
906 float tw, th;
907 dc.MeasureText(dc.theme->uiFont, 1.0f, 1.0f, text_.c_str(), &tw, &th);
908
909 if (tw > bounds_.w) {
910 dc.PushScissor(bounds_);
911 }
912
913 dc.SetFontStyle(dc.theme->uiFont);
914
915 if (!text_.empty()) {
916 float textX = bounds_.x + paddingW_ * 2.0f + radioRadius_ * 2.0f;
917 dc.DrawText(text_.c_str(), textX, bounds_.centerY(), style.fgColor, ALIGN_LEFT | ALIGN_VCENTER);
918 }
919
920 if (tw > bounds_.w) {
921 dc.PopScissor();
922 }
923 }
924
GetContentDimensions(const UIContext & dc,float & w,float & h) const925 void ImageView::GetContentDimensions(const UIContext &dc, float &w, float &h) const {
926 dc.Draw()->GetAtlas()->measureImage(atlasImage_, &w, &h);
927 // TODO: involve sizemode
928 }
929
Draw(UIContext & dc)930 void ImageView::Draw(UIContext &dc) {
931 const AtlasImage *img = dc.Draw()->GetAtlas()->getImage(atlasImage_);
932 if (img) {
933 // TODO: involve sizemode
934 float scale = bounds_.w / img->w;
935 dc.Draw()->DrawImage(atlasImage_, bounds_.x, bounds_.y, scale, 0xFFFFFFFF, ALIGN_TOPLEFT);
936 }
937 }
938
939 const float bulletOffset = 25;
940
GetContentDimensionsBySpec(const UIContext & dc,MeasureSpec horiz,MeasureSpec vert,float & w,float & h) const941 void TextView::GetContentDimensionsBySpec(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert, float &w, float &h) const {
942 Bounds bounds(0, 0, layoutParams_->width, layoutParams_->height);
943 if (bounds.w < 0) {
944 // If there's no size, let's grow as big as we want.
945 bounds.w = horiz.size == 0 ? MAX_ITEM_SIZE : horiz.size;
946 }
947 if (bounds.h < 0) {
948 bounds.h = vert.size == 0 ? MAX_ITEM_SIZE : vert.size;
949 }
950 ApplyBoundsBySpec(bounds, horiz, vert);
951 if (bullet_) {
952 bounds.w -= bulletOffset;
953 }
954 dc.MeasureTextRect(small_ ? dc.theme->uiFontSmall : dc.theme->uiFont, 1.0f, 1.0f, text_.c_str(), (int)text_.length(), bounds, &w, &h, textAlign_);
955
956 if (bullet_) {
957 w += bulletOffset;
958 }
959 }
960
Draw(UIContext & dc)961 void TextView::Draw(UIContext &dc) {
962 uint32_t textColor = hasTextColor_ ? textColor_ : dc.theme->infoStyle.fgColor;
963 if (!(textColor & 0xFF000000))
964 return;
965
966 bool clip = false;
967 if (measuredWidth_ > bounds_.w || measuredHeight_ > bounds_.h)
968 clip = true;
969 if (bounds_.w < 0 || bounds_.h < 0 || !clip_) {
970 // We have a layout but, but try not to screw up rendering.
971 // TODO: Fix properly.
972 clip = false;
973 }
974 if (clip) {
975 dc.Flush();
976 dc.PushScissor(bounds_);
977 }
978 // In case it's been made focusable.
979 if (HasFocus()) {
980 UI::Style style = dc.theme->itemFocusedStyle;
981 style.background.color &= 0x7fffffff;
982 dc.FillRect(style.background, bounds_);
983 }
984 dc.SetFontStyle(small_ ? dc.theme->uiFontSmall : dc.theme->uiFont);
985
986 Bounds textBounds = bounds_;
987
988 if (bullet_) {
989 float radius = 7.0f;
990 dc.Flush();
991 dc.BeginNoTex();
992 dc.Draw()->FillCircle(textBounds.x + radius, textBounds.centerY(), radius, 20, textColor);
993 dc.Flush();
994 dc.Begin();
995 textBounds.x += bulletOffset;
996 textBounds.w -= bulletOffset;
997 }
998
999 if (shadow_) {
1000 uint32_t shadowColor = 0x80000000;
1001 dc.DrawTextRect(text_.c_str(), textBounds.Offset(1.0f, 1.0f), shadowColor, textAlign_);
1002 }
1003 dc.DrawTextRect(text_.c_str(), textBounds, textColor, textAlign_);
1004 if (small_) {
1005 // If we changed font style, reset it.
1006 dc.SetFontStyle(dc.theme->uiFont);
1007 }
1008 if (clip) {
1009 dc.PopScissor();
1010 }
1011 }
1012
TextEdit(const std::string & text,const std::string & title,const std::string & placeholderText,LayoutParams * layoutParams)1013 TextEdit::TextEdit(const std::string &text, const std::string &title, const std::string &placeholderText, LayoutParams *layoutParams)
1014 : View(layoutParams), text_(text), title_(title), undo_(text), placeholderText_(placeholderText),
1015 textColor_(0xFFFFFFFF), maxLen_(255) {
1016 caret_ = (int)text_.size();
1017 }
1018
Draw(UIContext & dc)1019 void TextEdit::Draw(UIContext &dc) {
1020 dc.PushScissor(bounds_);
1021 dc.SetFontStyle(dc.theme->uiFont);
1022 dc.FillRect(HasFocus() ? UI::Drawable(0x80000000) : UI::Drawable(0x30000000), bounds_);
1023
1024 uint32_t textColor = hasTextColor_ ? textColor_ : dc.theme->infoStyle.fgColor;
1025 float textX = bounds_.x;
1026 float w, h;
1027
1028 Bounds textBounds = bounds_;
1029 textBounds.x = textX - scrollPos_;
1030
1031 if (text_.empty()) {
1032 if (placeholderText_.size()) {
1033 uint32_t c = textColor & 0x50FFFFFF;
1034 dc.DrawTextRect(placeholderText_.c_str(), bounds_, c, ALIGN_CENTER);
1035 }
1036 } else {
1037 dc.DrawTextRect(text_.c_str(), textBounds, textColor, ALIGN_VCENTER | ALIGN_LEFT | align_);
1038 }
1039
1040 if (HasFocus()) {
1041 // Hack to find the caret position. Might want to find a better way...
1042 dc.MeasureTextCount(dc.theme->uiFont, 1.0f, 1.0f, text_.c_str(), caret_, &w, &h, ALIGN_VCENTER | ALIGN_LEFT | align_);
1043 float caretX = w - scrollPos_;
1044 if (caretX > bounds_.w) {
1045 scrollPos_ += caretX - bounds_.w;
1046 }
1047 if (caretX < 0) {
1048 scrollPos_ += caretX;
1049 }
1050 caretX += textX;
1051 dc.FillRect(UI::Drawable(textColor), Bounds(caretX - 1, bounds_.y + 2, 3, bounds_.h - 4));
1052 }
1053 dc.PopScissor();
1054 }
1055
GetContentDimensions(const UIContext & dc,float & w,float & h) const1056 void TextEdit::GetContentDimensions(const UIContext &dc, float &w, float &h) const {
1057 dc.MeasureText(dc.theme->uiFont, 1.0f, 1.0f, text_.size() ? text_.c_str() : "Wj", &w, &h, align_);
1058 w += 2;
1059 h += 2;
1060 }
1061
DescribeText() const1062 std::string TextEdit::DescribeText() const {
1063 auto u = GetI18NCategory("UI Elements");
1064 return ReplaceAll(u->T("%1 text field"), "%1", GetText());
1065 }
1066
1067 // Handles both windows and unix line endings.
FirstLine(const std::string & text)1068 static std::string FirstLine(const std::string &text) {
1069 size_t pos = text.find("\r\n");
1070 if (pos != std::string::npos) {
1071 return text.substr(0, pos);
1072 }
1073 pos = text.find('\n');
1074 if (pos != std::string::npos) {
1075 return text.substr(0, pos);
1076 }
1077 return text;
1078 }
1079
Touch(const TouchInput & touch)1080 void TextEdit::Touch(const TouchInput &touch) {
1081 if (touch.flags & TOUCH_DOWN) {
1082 if (bounds_.Contains(touch.x, touch.y)) {
1083 SetFocusedView(this, true);
1084 }
1085 }
1086 }
1087
Key(const KeyInput & input)1088 bool TextEdit::Key(const KeyInput &input) {
1089 if (!HasFocus())
1090 return false;
1091 bool textChanged = false;
1092 // Process hardcoded navigation keys. These aren't chars.
1093 if (input.flags & KEY_DOWN) {
1094 switch (input.keyCode) {
1095 case NKCODE_CTRL_LEFT:
1096 case NKCODE_CTRL_RIGHT:
1097 ctrlDown_ = true;
1098 break;
1099 case NKCODE_DPAD_LEFT: // ASCII left arrow
1100 u8_dec(text_.c_str(), &caret_);
1101 break;
1102 case NKCODE_DPAD_RIGHT: // ASCII right arrow
1103 u8_inc(text_.c_str(), &caret_);
1104 break;
1105 case NKCODE_MOVE_HOME:
1106 case NKCODE_PAGE_UP:
1107 caret_ = 0;
1108 break;
1109 case NKCODE_MOVE_END:
1110 case NKCODE_PAGE_DOWN:
1111 caret_ = (int)text_.size();
1112 break;
1113 case NKCODE_FORWARD_DEL:
1114 if (caret_ < (int)text_.size()) {
1115 int endCaret = caret_;
1116 u8_inc(text_.c_str(), &endCaret);
1117 undo_ = text_;
1118 text_.erase(text_.begin() + caret_, text_.begin() + endCaret);
1119 textChanged = true;
1120 }
1121 break;
1122 case NKCODE_DEL:
1123 if (caret_ > 0) {
1124 int begCaret = caret_;
1125 u8_dec(text_.c_str(), &begCaret);
1126 undo_ = text_;
1127 text_.erase(text_.begin() + begCaret, text_.begin() + caret_);
1128 caret_--;
1129 textChanged = true;
1130 }
1131 break;
1132 case NKCODE_ENTER:
1133 case NKCODE_NUMPAD_ENTER:
1134 {
1135 EventParams e{};
1136 e.v = this;
1137 e.s = text_;
1138 OnEnter.Trigger(e);
1139 break;
1140 }
1141 case NKCODE_BACK:
1142 case NKCODE_ESCAPE:
1143 return false;
1144 }
1145
1146 if (ctrlDown_) {
1147 switch (input.keyCode) {
1148 case NKCODE_C:
1149 // Just copy the entire text contents, until we get selection support.
1150 System_SendMessage("setclipboardtext", text_.c_str());
1151 break;
1152 case NKCODE_V:
1153 {
1154 std::string clipText = System_GetProperty(SYSPROP_CLIPBOARD_TEXT);
1155 clipText = FirstLine(clipText);
1156 if (clipText.size()) {
1157 // Until we get selection, replace the whole text
1158 undo_ = text_;
1159 text_.clear();
1160 caret_ = 0;
1161
1162 size_t maxPaste = maxLen_ - text_.size();
1163 if (clipText.size() > maxPaste) {
1164 int end = 0;
1165 while ((size_t)end < maxPaste) {
1166 u8_inc(clipText.c_str(), &end);
1167 }
1168 if (end > 0) {
1169 u8_dec(clipText.c_str(), &end);
1170 }
1171 clipText = clipText.substr(0, end);
1172 }
1173 InsertAtCaret(clipText.c_str());
1174 textChanged = true;
1175 }
1176 }
1177 break;
1178 case NKCODE_Z:
1179 text_ = undo_;
1180 break;
1181 }
1182 }
1183
1184 if (caret_ < 0) {
1185 caret_ = 0;
1186 }
1187 if (caret_ > (int)text_.size()) {
1188 caret_ = (int)text_.size();
1189 }
1190 }
1191
1192 if (input.flags & KEY_UP) {
1193 switch (input.keyCode) {
1194 case NKCODE_CTRL_LEFT:
1195 case NKCODE_CTRL_RIGHT:
1196 ctrlDown_ = false;
1197 break;
1198 }
1199 }
1200
1201 // Process chars.
1202 if (input.flags & KEY_CHAR) {
1203 int unichar = input.keyCode;
1204 if (unichar >= 0x20 && !ctrlDown_) { // Ignore control characters.
1205 // Insert it! (todo: do it with a string insert)
1206 char buf[8];
1207 buf[u8_wc_toutf8(buf, unichar)] = '\0';
1208 if (strlen(buf) + text_.size() < maxLen_) {
1209 undo_ = text_;
1210 InsertAtCaret(buf);
1211 textChanged = true;
1212 }
1213 }
1214 }
1215
1216 if (textChanged) {
1217 UI::EventParams e{};
1218 e.v = this;
1219 OnTextChange.Trigger(e);
1220 }
1221 return true;
1222 }
1223
InsertAtCaret(const char * text)1224 void TextEdit::InsertAtCaret(const char *text) {
1225 size_t len = strlen(text);
1226 for (size_t i = 0; i < len; i++) {
1227 text_.insert(text_.begin() + caret_, text[i]);
1228 caret_++;
1229 }
1230 }
1231
GetContentDimensions(const UIContext & dc,float & w,float & h) const1232 void ProgressBar::GetContentDimensions(const UIContext &dc, float &w, float &h) const {
1233 dc.MeasureText(dc.theme->uiFont, 1.0f, 1.0f, " 100% ", &w, &h);
1234 }
1235
Draw(UIContext & dc)1236 void ProgressBar::Draw(UIContext &dc) {
1237 char temp[32];
1238 sprintf(temp, "%i%%", (int)(progress_ * 100.0f));
1239 dc.Draw()->DrawImageCenterTexel(dc.theme->whiteImage, bounds_.x, bounds_.y, bounds_.x + bounds_.w * progress_, bounds_.y2(), 0xc0c0c0c0);
1240 dc.SetFontStyle(dc.theme->uiFont);
1241 dc.DrawTextRect(temp, bounds_, 0xFFFFFFFF, ALIGN_CENTER);
1242 }
1243
DescribeText() const1244 std::string ProgressBar::DescribeText() const {
1245 auto u = GetI18NCategory("UI Elements");
1246 float percent = progress_ * 100.0f;
1247 return ReplaceAll(u->T("Progress: %1%"), "%1", StringFromInt((int)percent));
1248 }
1249
GetContentDimensions(const UIContext & dc,float & w,float & h) const1250 void Spinner::GetContentDimensions(const UIContext &dc, float &w, float &h) const {
1251 w = 48;
1252 h = 48;
1253 }
1254
Draw(UIContext & dc)1255 void Spinner::Draw(UIContext &dc) {
1256 if (!(color_ & 0xFF000000))
1257 return;
1258 double t = time_now_d() * 1.3f;
1259 double angle = fmod(t, M_PI * 2.0);
1260 float r = bounds_.w * 0.5f;
1261 double da = M_PI * 2.0 / numImages_;
1262 for (int i = 0; i < numImages_; i++) {
1263 double a = angle + i * da;
1264 float x = (float)cos(a) * r;
1265 float y = (float)sin(a) * r;
1266 dc.Draw()->DrawImage(images_[i], bounds_.centerX() + x, bounds_.centerY() + y, 1.0f, color_, ALIGN_CENTER);
1267 }
1268 }
1269
Touch(const TouchInput & input)1270 void TriggerButton::Touch(const TouchInput &input) {
1271 if (input.flags & TOUCH_DOWN) {
1272 if (bounds_.Contains(input.x, input.y)) {
1273 down_ |= 1 << input.id;
1274 }
1275 }
1276 if (input.flags & TOUCH_MOVE) {
1277 if (bounds_.Contains(input.x, input.y))
1278 down_ |= 1 << input.id;
1279 else
1280 down_ &= ~(1 << input.id);
1281 }
1282
1283 if (input.flags & TOUCH_UP) {
1284 down_ &= ~(1 << input.id);
1285 }
1286
1287 if (down_ != 0) {
1288 *bitField_ |= bit_;
1289 } else {
1290 *bitField_ &= ~bit_;
1291 }
1292 }
1293
Draw(UIContext & dc)1294 void TriggerButton::Draw(UIContext &dc) {
1295 dc.Draw()->DrawImage(imageBackground_, bounds_.centerX(), bounds_.centerY(), 1.0f, 0xFFFFFFFF, ALIGN_CENTER);
1296 dc.Draw()->DrawImage(imageForeground_, bounds_.centerX(), bounds_.centerY(), 1.0f, 0xFFFFFFFF, ALIGN_CENTER);
1297 }
1298
GetContentDimensions(const UIContext & dc,float & w,float & h) const1299 void TriggerButton::GetContentDimensions(const UIContext &dc, float &w, float &h) const {
1300 dc.Draw()->GetAtlas()->measureImage(imageBackground_, &w, &h);
1301 }
1302
Key(const KeyInput & input)1303 bool Slider::Key(const KeyInput &input) {
1304 if (HasFocus() && (input.flags & (KEY_DOWN | KEY_IS_REPEAT)) == KEY_DOWN) {
1305 if (ApplyKey(input.keyCode)) {
1306 Clamp();
1307 repeat_ = 0;
1308 repeatCode_ = input.keyCode;
1309 return true;
1310 }
1311 return false;
1312 } else if ((input.flags & KEY_UP) && input.keyCode == repeatCode_) {
1313 repeat_ = -1;
1314 return false;
1315 } else {
1316 return false;
1317 }
1318 }
1319
ApplyKey(int keyCode)1320 bool Slider::ApplyKey(int keyCode) {
1321 switch (keyCode) {
1322 case NKCODE_DPAD_LEFT:
1323 case NKCODE_MINUS:
1324 case NKCODE_NUMPAD_SUBTRACT:
1325 *value_ -= step_;
1326 break;
1327 case NKCODE_DPAD_RIGHT:
1328 case NKCODE_PLUS:
1329 case NKCODE_NUMPAD_ADD:
1330 *value_ += step_;
1331 break;
1332 case NKCODE_PAGE_UP:
1333 *value_ -= step_ * 10;
1334 break;
1335 case NKCODE_PAGE_DOWN:
1336 *value_ += step_ * 10;
1337 break;
1338 case NKCODE_MOVE_HOME:
1339 *value_ = minValue_;
1340 break;
1341 case NKCODE_MOVE_END:
1342 *value_ = maxValue_;
1343 break;
1344 default:
1345 return false;
1346 }
1347 return true;
1348 }
1349
Touch(const TouchInput & input)1350 void Slider::Touch(const TouchInput &input) {
1351 // Calling it afterwards, so dragging_ hasn't been set false yet when checking it above.
1352 Clickable::Touch(input);
1353 if (dragging_) {
1354 float relativeX = (input.x - (bounds_.x + paddingLeft_)) / (bounds_.w - paddingLeft_ - paddingRight_);
1355 *value_ = floorf(relativeX * (maxValue_ - minValue_) + minValue_ + 0.5f);
1356 Clamp();
1357 EventParams params{};
1358 params.v = this;
1359 params.a = (uint32_t)(*value_);
1360 params.f = (float)(*value_);
1361 OnChange.Trigger(params);
1362 }
1363
1364 // Cancel any key repeat.
1365 repeat_ = -1;
1366 }
1367
Clamp()1368 void Slider::Clamp() {
1369 if (*value_ < minValue_) *value_ = minValue_;
1370 else if (*value_ > maxValue_) *value_ = maxValue_;
1371
1372 // Clamp the value to be a multiple of the nearest step (e.g. if step == 5, value == 293, it'll round down to 290).
1373 *value_ = *value_ - fmodf(*value_, step_);
1374 }
1375
Draw(UIContext & dc)1376 void Slider::Draw(UIContext &dc) {
1377 bool focus = HasFocus();
1378 uint32_t linecolor = dc.theme->popupTitle.fgColor;
1379 Style knobStyle = (down_ || focus) ? dc.theme->popupTitle : dc.theme->popupStyle;
1380
1381 float knobX = ((float)(*value_) - minValue_) / (maxValue_ - minValue_) * (bounds_.w - paddingLeft_ - paddingRight_) + (bounds_.x + paddingLeft_);
1382 dc.FillRect(Drawable(linecolor), Bounds(bounds_.x + paddingLeft_, bounds_.centerY() - 2, knobX - (bounds_.x + paddingLeft_), 4));
1383 dc.FillRect(Drawable(0xFF808080), Bounds(knobX, bounds_.centerY() - 2, (bounds_.x + bounds_.w - paddingRight_ - knobX), 4));
1384 dc.Draw()->DrawImage(dc.theme->sliderKnob, knobX, bounds_.centerY(), 1.0f, knobStyle.fgColor, ALIGN_CENTER);
1385 char temp[64];
1386 if (showPercent_)
1387 sprintf(temp, "%i%%", *value_);
1388 else
1389 sprintf(temp, "%i", *value_);
1390 dc.SetFontStyle(dc.theme->uiFont);
1391 dc.DrawText(temp, bounds_.x2() - 22, bounds_.centerY(), dc.theme->popupStyle.fgColor, ALIGN_CENTER | FLAG_DYNAMIC_ASCII);
1392 }
1393
DescribeText() const1394 std::string Slider::DescribeText() const {
1395 if (showPercent_)
1396 return StringFromFormat("%i%% / %i%%", *value_, maxValue_);
1397 return StringFromFormat("%i / %i", *value_, maxValue_);
1398 }
1399
Update()1400 void Slider::Update() {
1401 View::Update();
1402 if (repeat_ >= 0) {
1403 repeat_++;
1404 }
1405
1406 if (repeat_ >= 47) {
1407 ApplyKey(repeatCode_);
1408 if ((maxValue_ - minValue_) / step_ >= 300) {
1409 ApplyKey(repeatCode_);
1410 }
1411 Clamp();
1412 } else if (repeat_ >= 12 && (repeat_ & 1) == 1) {
1413 ApplyKey(repeatCode_);
1414 Clamp();
1415 }
1416 }
1417
GetContentDimensions(const UIContext & dc,float & w,float & h) const1418 void Slider::GetContentDimensions(const UIContext &dc, float &w, float &h) const {
1419 // TODO
1420 w = 100;
1421 h = 50;
1422 }
1423
Key(const KeyInput & input)1424 bool SliderFloat::Key(const KeyInput &input) {
1425 if (HasFocus() && (input.flags & (KEY_DOWN | KEY_IS_REPEAT)) == KEY_DOWN) {
1426 if (ApplyKey(input.keyCode)) {
1427 Clamp();
1428 repeat_ = 0;
1429 repeatCode_ = input.keyCode;
1430 return true;
1431 }
1432 return false;
1433 } else if ((input.flags & KEY_UP) && input.keyCode == repeatCode_) {
1434 repeat_ = -1;
1435 return false;
1436 } else {
1437 return false;
1438 }
1439 }
1440
ApplyKey(int keyCode)1441 bool SliderFloat::ApplyKey(int keyCode) {
1442 switch (keyCode) {
1443 case NKCODE_DPAD_LEFT:
1444 case NKCODE_MINUS:
1445 case NKCODE_NUMPAD_SUBTRACT:
1446 *value_ -= (maxValue_ - minValue_) / 50.0f;
1447 break;
1448 case NKCODE_DPAD_RIGHT:
1449 case NKCODE_PLUS:
1450 case NKCODE_NUMPAD_ADD:
1451 *value_ += (maxValue_ - minValue_) / 50.0f;
1452 break;
1453 case NKCODE_PAGE_UP:
1454 *value_ -= (maxValue_ - minValue_) / 5.0f;
1455 break;
1456 case NKCODE_PAGE_DOWN:
1457 *value_ += (maxValue_ - minValue_) / 5.0f;
1458 break;
1459 case NKCODE_MOVE_HOME:
1460 *value_ = minValue_;
1461 break;
1462 case NKCODE_MOVE_END:
1463 *value_ = maxValue_;
1464 break;
1465 default:
1466 return false;
1467 }
1468 return true;
1469 }
1470
Touch(const TouchInput & input)1471 void SliderFloat::Touch(const TouchInput &input) {
1472 Clickable::Touch(input);
1473 if (dragging_) {
1474 float relativeX = (input.x - (bounds_.x + paddingLeft_)) / (bounds_.w - paddingLeft_ - paddingRight_);
1475 *value_ = (relativeX * (maxValue_ - minValue_) + minValue_);
1476 Clamp();
1477 EventParams params{};
1478 params.v = this;
1479 params.a = (uint32_t)(*value_);
1480 params.f = (float)(*value_);
1481 OnChange.Trigger(params);
1482 }
1483
1484 // Cancel any key repeat.
1485 repeat_ = -1;
1486 }
1487
Clamp()1488 void SliderFloat::Clamp() {
1489 if (*value_ < minValue_)
1490 *value_ = minValue_;
1491 else if (*value_ > maxValue_)
1492 *value_ = maxValue_;
1493 }
1494
Draw(UIContext & dc)1495 void SliderFloat::Draw(UIContext &dc) {
1496 bool focus = HasFocus();
1497 uint32_t linecolor = dc.theme->popupTitle.fgColor;
1498 Style knobStyle = (down_ || focus) ? dc.theme->popupTitle : dc.theme->popupStyle;
1499
1500 float knobX = (*value_ - minValue_) / (maxValue_ - minValue_) * (bounds_.w - paddingLeft_ - paddingRight_) + (bounds_.x + paddingLeft_);
1501 dc.FillRect(Drawable(linecolor), Bounds(bounds_.x + paddingLeft_, bounds_.centerY() - 2, knobX - (bounds_.x + paddingLeft_), 4));
1502 dc.FillRect(Drawable(0xFF808080), Bounds(knobX, bounds_.centerY() - 2, (bounds_.x + bounds_.w - paddingRight_ - knobX), 4));
1503 dc.Draw()->DrawImage(dc.theme->sliderKnob, knobX, bounds_.centerY(), 1.0f, knobStyle.fgColor, ALIGN_CENTER);
1504 char temp[64];
1505 sprintf(temp, "%0.2f", *value_);
1506 dc.SetFontStyle(dc.theme->uiFont);
1507 dc.DrawText(temp, bounds_.x2() - 22, bounds_.centerY(), dc.theme->popupStyle.fgColor, ALIGN_CENTER);
1508 }
1509
DescribeText() const1510 std::string SliderFloat::DescribeText() const {
1511 return StringFromFormat("%0.2f / %0.2f", *value_, maxValue_);
1512 }
1513
Update()1514 void SliderFloat::Update() {
1515 View::Update();
1516 if (repeat_ >= 0) {
1517 repeat_++;
1518 }
1519
1520 if (repeat_ >= 47) {
1521 ApplyKey(repeatCode_);
1522 Clamp();
1523 } else if (repeat_ >= 12 && (repeat_ & 1) == 1) {
1524 ApplyKey(repeatCode_);
1525 Clamp();
1526 }
1527 }
1528
GetContentDimensions(const UIContext & dc,float & w,float & h) const1529 void SliderFloat::GetContentDimensions(const UIContext &dc, float &w, float &h) const {
1530 // TODO
1531 w = 100;
1532 h = 50;
1533 }
1534
1535 } // namespace
1536