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