OXYGENHUB
◆ main openmsx / src / console / OSDImageBasedWidget.cc
9.7 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
#include "OSDImageBasedWidget.hh"
2
3
#include "OSDGUI.hh"
4
#include "OSDTopWidget.hh"
5
6
#include "CommandException.hh"
7
#include "Display.hh"
8
#include "GLImage.hh"
9
#include "TclObject.hh"
10
#include "Timer.hh"
11
12
#include "narrow.hh"
13
#include "ranges.hh"
14
#include "stl.hh"
15
#include "xrange.hh"
16
17
#include <algorithm>
18
#include <cassert>
19
#include <cstdint>
20
#include <ranges>
21
22
using namespace gl;
23
24
namespace openmsx {
25
26
OSDImageBasedWidget::OSDImageBasedWidget(Display& display_, const TclObject& name_)
27
	: OSDWidget(display_, name_)
28
{
29
	std::ranges::fill(rgba, 0x000000ff); // black, opaque
30
}
31
32
OSDImageBasedWidget::~OSDImageBasedWidget() = default;
33
34
[[nodiscard]] static std::array<uint32_t, 4> get4(Interpreter& interp, const TclObject& value)
35
{
36
	std::array<uint32_t, 4> result;
37
	auto len = value.getListLength(interp);
38
	if (len == 4) {
39
		for (auto i : xrange(4)) {
40
			result[i] = value.getListIndex(interp, i).getInt(interp);
41
		}
42
	} else if (len == 1) {
43
		std::ranges::fill(result, value.getInt(interp));
44
	} else {
45
		throw CommandException("Expected either 1 or 4 values.");
46
	}
47
	return result;
48
}
49
void OSDImageBasedWidget::setProperty(
50
	Interpreter& interp, std::string_view propName, const TclObject& value)
51
{
52
	if (propName == "-rgba") {
53
		std::array<uint32_t, 4> newRGBA = get4(interp, value);
54
		setRGBA(newRGBA);
55
	} else if (propName == "-rgb") {
56
		std::array<uint32_t, 4> newRGB = get4(interp, value);
57
		std::array<uint32_t, 4> newRGBA;
58
		for (auto i : xrange(4)) {
59
			newRGBA[i] = (rgba[i]          & 0x000000ff) |
60
			             ((newRGB[i] << 8) & 0xffffff00);
61
		}
62
		setRGBA(newRGBA);
63
	} else if (propName == "-alpha") {
64
		std::array<uint32_t, 4> newAlpha = get4(interp, value);
65
		std::array<uint32_t, 4> newRGBA;
66
		for (auto i : xrange(4)) {
67
			newRGBA[i] = (rgba[i]     & 0xffffff00) |
68
			             (newAlpha[i] & 0x000000ff);
69
		}
70
		setRGBA(newRGBA);
71
	} else if (propName == "-fadePeriod") {
72
		updateCurrentFadeValue();
73
		fadePeriod = value.getFloat(interp);
74
	} else if (propName == "-fadeTarget") {
75
		updateCurrentFadeValue();
76
		fadeTarget = std::clamp(value.getFloat(interp), 0.0f, 1.0f);
77
	} else if (propName == "-fadeCurrent") {
78
		startFadeValue = std::clamp(value.getFloat(interp), 0.0f, 1.0f);
79
		startFadeTime = Timer::getTime();
80
	} else if (propName == "-scrollSpeed") {
81
		scrollSpeed = std::max(0.0f, value.getFloat(interp));
82
		startScrollTime = Timer::getTime();
83
	} else if (propName == "-scrollPauseLeft") {
84
		scrollPauseLeft = std::max(0.0f, value.getFloat(interp));
85
	} else if (propName == "-scrollPauseRight") {
86
		scrollPauseRight = std::max(0.0f, value.getFloat(interp));
87
	} else if (propName == "-query-size") {
88
		throw CommandException("-query-size property is readonly");
89
	} else {
90
		OSDWidget::setProperty(interp, propName, value);
91
	}
92
}
93
94
void OSDImageBasedWidget::setRGBA(std::span<const uint32_t, 4> newRGBA)
95
{
96
	if (std::ranges::equal(rgba, newRGBA)) {
97
		return; // not changed
98
	}
99
	invalidateLocal();
100
	copy_to_range(newRGBA, rgba);
101
}
102
103
static void set4(std::span<const uint32_t, 4> rgba, uint32_t mask, unsigned shift, TclObject& result)
104
{
105
	if (ranges::all_equal(rgba)) {
106
		result = (rgba[0] & mask) >> shift;
107
	} else {
108
		result.addListElements(std::views::transform(xrange(4), [&](auto i) {
109
			return int((rgba[i] & mask) >> shift);
110
		}));
111
	}
112
}
113
void OSDImageBasedWidget::getProperty(std::string_view propName, TclObject& result) const
114
{
115
	if (propName == "-rgba") {
116
		set4(rgba, 0xffffffff, 0, result);
117
	} else if (propName == "-rgb") {
118
		set4(rgba, 0xffffff00, 8, result);
119
	} else if (propName == "-alpha") {
120
		set4(rgba, 0x000000ff, 0, result);
121
	} else if (propName == "-fadePeriod") {
122
		result = fadePeriod;
123
	} else if (propName == "-fadeTarget") {
124
		result = fadeTarget;
125
	} else if (propName == "-fadeCurrent") {
126
		result = getCurrentFadeValue();
127
	} else if (propName == "-scrollSpeed") {
128
		result = scrollSpeed;
129
	} else if (propName == "-scrollPauseLeft") {
130
		result = scrollPauseLeft;
131
	} else if (propName == "-scrollPauseRight") {
132
		result = scrollPauseRight;
133
	} else if (propName == "-query-size") {
134
		auto [w, h] = getRenderedSize();
135
		result.addListElement(w, h);
136
	} else {
137
		OSDWidget::getProperty(propName, result);
138
	}
139
}
140
141
std::optional<float> OSDImageBasedWidget::getScrollWidth() const
142
{
143
        if (scrollSpeed == 0.0f) return {};
144
145
        const auto* parentImage = dynamic_cast<const OSDImageBasedWidget*>(getParent());
146
        if (!parentImage) return {};
147
148
        const auto* output = getDisplay().getOutputSurface();
149
        if (!output) return {};
150
151
        auto [parentPos, parentSize] = parentImage->getBoundingBox(*output);
152
        auto parentWidth = parentSize.x / narrow<float>(getScaleFactor(*output));
153
154
        auto thisWidth = getRenderedSize().x;
155
        auto scrollWidth = thisWidth - parentWidth;
156
        if (scrollWidth <= 0.0f) return {};
157
158
        return scrollWidth;
159
}
160
161
bool OSDImageBasedWidget::isAnimating() const
162
{
163
	return static_cast<bool>(getScrollWidth());
164
}
165
166
[[nodiscard]] static float smootherStep(float x)
167
{
168
	// https://en.wikipedia.org/wiki/Smoothstep
169
	//    6x^5 - 15x^4 + 10x^3
170
	return ((6.0f * x - 15.0f) * x + 10.0f) * x * x * x;
171
}
172
173
gl::vec2 OSDImageBasedWidget::getPos() const
174
{
175
	// get the original position, possibly this gets modified because of scrolling
176
	auto result = OSDWidget::getPos();
177
178
	auto width = getScrollWidth();
179
	if (!width) return result;
180
181
	auto scrollTime = *width / scrollSpeed;
182
	auto animationTime = 2.0f * scrollTime + scrollPauseLeft + scrollPauseRight;
183
184
	// transform moment in time to animation-timestamp 't'
185
	auto now = narrow_cast<float>(Timer::getTime() - startScrollTime) / 1'000'000.0f;
186
	auto t = fmodf(now, animationTime);
187
188
	// transform animation timestamp to position
189
	float relOffsetX = [&]{
190
		if (t < scrollPauseLeft) {
191
			// no scrolling yet, pausing at the left
192
			return 0.0f;
193
		} else if (t < (scrollPauseLeft + scrollTime)) {
194
			// scrolling to the left
195
			return smootherStep((t - scrollPauseLeft) / scrollTime);
196
		} else if (t < (scrollPauseLeft + scrollTime + scrollPauseRight)) {
197
			// no scrolling yet, pausing at the right
198
			return 1.0f;
199
		} else {
200
			// scrolling to the right
201
			return smootherStep(1.0f - ((t - scrollPauseLeft - scrollTime - scrollPauseRight) / scrollTime));
202
		}
203
	}();
204
	result.x -= *width * relOffsetX;
205
	return result;
206
}
207
208
bool OSDImageBasedWidget::hasConstantAlpha() const
209
{
210
	return ranges::all_equal(rgba, [](auto c) { return c & 0xff; });
211
}
212
213
float OSDImageBasedWidget::getRecursiveFadeValue() const
214
{
215
	return getParent()->getRecursiveFadeValue() * getCurrentFadeValue();
216
}
217
218
bool OSDImageBasedWidget::isVisible() const
219
{
220
	return (getFadedAlpha() != 0) || isRecursiveFading();
221
}
222
223
bool OSDImageBasedWidget::isFading() const
224
{
225
	return (startFadeValue != fadeTarget) && (fadePeriod != 0.0f);
226
}
227
228
bool OSDImageBasedWidget::isRecursiveFading() const
229
{
230
	if (isFading()) return true;
231
	return getParent()->isRecursiveFading();
232
}
233
234
float OSDImageBasedWidget::getCurrentFadeValue() const
235
{
236
	if (!isFading()) {
237
		return startFadeValue;
238
	}
239
	return getCurrentFadeValue(Timer::getTime());
240
}
241
242
float OSDImageBasedWidget::getCurrentFadeValue(uint64_t now) const
243
{
244
	assert(now >= startFadeTime);
245
246
	auto diff = narrow<int>(now - startFadeTime); // int should be big enough
247
	assert(fadePeriod != 0.0f);
248
	float delta = narrow_cast<float>(diff) / (1000000.0f * fadePeriod);
249
	if (startFadeValue < fadeTarget) {
250
		float tmp = startFadeValue + delta;
251
		if (tmp >= fadeTarget) {
252
			startFadeValue = fadeTarget;
253
			return startFadeValue;
254
		}
255
		return tmp;
256
	} else {
257
		float tmp = startFadeValue - delta;
258
		if (tmp <= fadeTarget) {
259
			startFadeValue = fadeTarget;
260
			return startFadeValue;
261
		}
262
		return tmp;
263
	}
264
}
265
266
void OSDImageBasedWidget::updateCurrentFadeValue()
267
{
268
	auto now = Timer::getTime();
269
	if (isFading()) {
270
		startFadeValue = getCurrentFadeValue(now);
271
	}
272
	startFadeTime = now;
273
}
274
275
void OSDImageBasedWidget::invalidateLocal()
276
{
277
	error = false;
278
	image.reset();
279
}
280
281
vec2 OSDImageBasedWidget::getTransformedPos(const OutputSurface& output) const
282
{
283
	return getParent()->transformPos(
284
		output, float(getScaleFactor(output)) * getPos(), getRelPos());
285
}
286
287
void OSDImageBasedWidget::setError(std::string message)
288
{
289
	error = true;
290
291
	// The suppressErrors property only exists to break an infinite loop
292
	// when an error occurs (e.g. couldn't load font) while displaying the
293
	// error message on the OSD system.
294
	// The difficulty in detecting this loop is that it's not a recursive
295
	// loop, but each iteration takes one frame: on the CliComm Tcl callback,
296
	// the OSD widgets get created, but only the next frame, when this new
297
	// widget is actually drawn the next error occurs.
298
	if (!needSuppressErrors()) {
299
		getDisplay().getOSDGUI().getTopWidget().queueError(std::move(message));
300
	}
301
}
302
303
void OSDImageBasedWidget::createImage(OutputSurface& output)
304
{
305
	if (!image && !hasError()) {
306
		try {
307
			image = create(output);
308
		} catch (MSXException& e) {
309
			setError(std::move(e).getMessage());
310
		}
311
	}
312
}
313
314
vec2 OSDImageBasedWidget::getRenderedSize() const
315
{
316
	auto* output = getDisplay().getOutputSurface();
317
	if (!output) {
318
		throw CommandException(
319
			"Can't query size: no window visible");
320
	}
321
	// force creating image (does not yet draw it on screen)
322
	const_cast<OSDImageBasedWidget*>(this)->createImage(*output);
323
324
	vec2 imageSize = [&] {
325
		if (image) {
326
			return vec2(image->getSize());
327
		} else {
328
			// Couldn't be rendered, maybe an (intentionally)
329
			// invisible rectangle
330
			return getBoundingBox(*output).size;
331
		}
332
	}();
333
	return imageSize / float(getScaleFactor(*output));
334
}
335
336
void OSDImageBasedWidget::paint(OutputSurface& output)
337
{
338
	// Note: Even when alpha == 0 we still create the image:
339
	//    It may be needed to get the dimensions to be able to position
340
	//    child widgets.
341
	createImage(output);
342
343
	if (auto fadedAlpha = getFadedAlpha();
344
	    (fadedAlpha != 0) && image) {
345
		ivec2 drawPos = round(getTransformedPos(output));
346
		image->draw(drawPos, fadedAlpha);
347
	}
348
	if (isRecursiveFading() || isAnimating()) {
349
		getDisplay().getOSDGUI().refresh();
350
	}
351
}
352
353
} // namespace openmsx