OXYGENHUB
◆ main openmsx / src / console / OSDWidget.cc
9.8 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
#include "OSDWidget.hh"
2
3
#include "CommandException.hh"
4
#include "Display.hh"
5
#include "GLUtil.hh"
6
#include "OutputSurface.hh"
7
#include "TclObject.hh"
8
#include "VideoSystem.hh"
9
10
#include "narrow.hh"
11
#include "stl.hh"
12
13
#include <SDL.h>
14
15
#include <algorithm>
16
#include <array>
17
#include <limits>
18
#include <optional>
19
20
using namespace gl;
21
22
namespace openmsx {
23
24
// intersect two rectangles
25
namespace { struct Rectangle { int x, y, w, h; }; }
26
static constexpr Rectangle intersect(const Rectangle& a, const Rectangle& b)
27
{
28
	int x1 = std::max<int>(a.x, b.x);
29
	int y1 = std::max<int>(a.y, b.y);
30
	int x2 = std::min<int>(a.x + a.w, b.x + b.w);
31
	int y2 = std::min<int>(a.y + a.h, b.y + b.h);
32
	int w = std::max(0, x2 - x1);
33
	int h = std::max(0, y2 - y1);
34
	return {.x = x1, .y = y1, .w = w, .h = h};
35
}
36
37
////
38
template<typename T>
39
static constexpr void normalize(T& x, T& w)
40
{
41
	if (w < 0) {
42
		w = -w;
43
		x -= w;
44
	}
45
}
46
47
48
class GLScopedClip
49
{
50
public:
51
	GLScopedClip(const OutputSurface& output, vec2 xy, vec2 wh);
52
	GLScopedClip(const GLScopedClip&) = delete;
53
	GLScopedClip(GLScopedClip&&) = delete;
54
	GLScopedClip& operator=(const GLScopedClip&) = delete;
55
	GLScopedClip& operator=(GLScopedClip&&) = delete;
56
	~GLScopedClip();
57
private:
58
	std::optional<std::array<GLint, 4>> origClip; // x, y, w, h;
59
};
60
61
62
GLScopedClip::GLScopedClip(const OutputSurface& output, vec2 xy, vec2 wh)
63
{
64
	auto& [x, y] = xy;
65
	auto& [w, h] = wh;
66
	normalize(x, w); normalize(y, h);
67
	y = narrow_cast<float>(output.getLogicalHeight()) - y - h; // openGL sets (0,0) in LOWER-left corner
68
69
	// transform view-space coordinates to clip-space coordinates
70
	vec2 scale = output.getViewScale();
71
	auto [ix, iy] = round(xy * scale) + output.getViewOffset();
72
	auto [iw, ih] = round(wh * scale);
73
74
	if (glIsEnabled(GL_SCISSOR_TEST) == GL_TRUE) {
75
		origClip.emplace();
76
		glGetIntegerv(GL_SCISSOR_BOX, origClip->data());
77
		auto [xn, yn, wn, hn] = intersect(
78
			Rectangle{.x = (*origClip)[0], .y = (*origClip)[1], .w = (*origClip)[2], .h = (*origClip)[3]},
79
			Rectangle{.x = ix, .y = iy, .w = iw, .h = ih});
80
		glScissor(xn, yn, wn, hn);
81
	} else {
82
		glScissor(ix, iy, iw, ih);
83
		glEnable(GL_SCISSOR_TEST);
84
	}
85
}
86
87
GLScopedClip::~GLScopedClip()
88
{
89
	if (origClip) {
90
		glScissor((*origClip)[0], (*origClip)[1], (*origClip)[2], (*origClip)[3]);
91
	} else {
92
		glDisable(GL_SCISSOR_TEST);
93
	}
94
}
95
96
////
97
98
OSDWidget::OSDWidget(Display& display_, TclObject name_)
99
	: display(display_)
100
	, name(std::move(name_))
101
{
102
}
103
104
void OSDWidget::addWidget(std::unique_ptr<OSDWidget> widget)
105
{
106
	widget->setParent(this);
107
108
	// Insert the new widget in the correct place (sorted on ascending Z)
109
	// heuristic: often we have either
110
	//  - many widgets with all the same Z
111
	//  - only a few total number of subWidgets (possibly with different Z)
112
	// In the former case we can simply append at the end. In the latter
113
	// case a linear search is probably faster than a binary search. Only
114
	// when there are many sub-widgets with not all the same Z (and not
115
	// created in sorted Z-order) a binary search would be faster.
116
	float widgetZ = widget->getZ();
117
	if (subWidgets.empty() || (subWidgets.back()->getZ() <= widgetZ)) {
118
		subWidgets.push_back(std::move(widget));
119
	} else {
120
		auto it = begin(subWidgets);
121
		while ((*it)->getZ() <= widgetZ) ++it;
122
		subWidgets.insert(it, std::move(widget));
123
124
	}
125
}
126
127
void OSDWidget::deleteWidget(OSDWidget& widget)
128
{
129
	auto it = rfind_unguarded(subWidgets, &widget,
130
	                          [](const auto& p) { return p.get(); });
131
	subWidgets.erase(it);
132
}
133
134
void OSDWidget::resortUp(const OSDWidget* elem)
135
{
136
	// z-coordinate was increased, first search for elements current position
137
	auto it1 = begin(subWidgets);
138
	while (it1->get() != elem) ++it1;
139
	// next search for the position were it belongs
140
	float elemZ = elem->getZ();
141
	auto it2 = it1;
142
	++it2;
143
	while ((it2 != end(subWidgets)) && ((*it2)->getZ() < elemZ)) ++it2;
144
	// now move elements to correct position
145
	rotate(it1, it1 + 1, it2);
146
#ifdef DEBUG
147
	assert(std::ranges::is_sorted(subWidgets, {}, &OSDWidget::getZ));
148
#endif
149
}
150
void OSDWidget::resortDown(const OSDWidget* elem)
151
{
152
	// z-coordinate was decreased, first search for new position
153
	auto it1 = begin(subWidgets);
154
	float elemZ = elem->getZ();
155
	while ((*it1)->getZ() <= elemZ) {
156
		++it1;
157
		if (it1 == end(subWidgets)) return;
158
	}
159
	// next search for the elements current position
160
	auto it2 = it1;
161
	if ((it2 != begin(subWidgets)) && ((it2 - 1)->get() == elem)) return;
162
	while (it2->get() != elem) ++it2;
163
	// now move elements to correct position
164
	rotate(it1, it2, it2 + 1);
165
#ifdef DEBUG
166
	assert(std::ranges::is_sorted(subWidgets, {}, &OSDWidget::getZ));
167
#endif
168
}
169
170
void OSDWidget::setProperty(
171
	Interpreter& interp, std::string_view propName, const TclObject& value)
172
{
173
	if (propName == "-type") {
174
		throw CommandException("-type property is readonly");
175
	} else if (propName == "-mousecoord") {
176
		throw CommandException("-mousecoord property is readonly");
177
	} else if (propName == "-x") {
178
		pos.x = value.getFloat(interp);
179
	} else if (propName == "-y") {
180
		pos.y = value.getFloat(interp);
181
	} else if (propName == "-z") {
182
		float z2 = value.getFloat(interp);
183
		if (z != z2) {
184
			bool up = z2 > z; // was z increased?
185
			z = z2;
186
			if (auto* p = getParent()) {
187
				// TODO no need for a full sort: instead remove and re-insert in the correct place
188
				if (up) {
189
					p->resortUp(this);
190
				} else {
191
					p->resortDown(this);
192
				}
193
			}
194
		}
195
	} else if (propName == "-relx") {
196
		relPos.x = value.getFloat(interp);
197
	} else if (propName == "-rely") {
198
		relPos.y = value.getFloat(interp);
199
	} else if (propName == "-scaled") {
200
		bool scaled2 = value.getBoolean(interp);
201
		if (scaled != scaled2) {
202
			scaled = scaled2;
203
			invalidateRecursive();
204
		}
205
	} else if (propName == "-clip") {
206
		clip = value.getBoolean(interp);
207
	} else if (propName == "-suppressErrors") {
208
		suppressErrors = value.getBoolean(interp);
209
	} else {
210
		throw CommandException("No such property: ", propName);
211
	}
212
}
213
214
void OSDWidget::getProperty(std::string_view propName, TclObject& result) const
215
{
216
	if (propName == "-type") {
217
		result = getType();
218
	} else if (propName == "-x") {
219
		result = pos.x;
220
	} else if (propName == "-y") {
221
		result = pos.y;
222
	} else if (propName == "-z") {
223
		result = z;
224
	} else if (propName == "-relx") {
225
		result = relPos.x;
226
	} else if (propName == "-rely") {
227
		result = relPos.y;
228
	} else if (propName == "-scaled") {
229
		result = scaled;
230
	} else if (propName == "-clip") {
231
		result = clip;
232
	} else if (propName == "-mousecoord") {
233
		auto [x, y] = getMouseCoord();
234
		result.addListElement(x, y);
235
	} else if (propName == "-suppressErrors") {
236
		result = suppressErrors;
237
	} else {
238
		throw CommandException("No such property: ", propName);
239
	}
240
}
241
242
float OSDWidget::getRecursiveFadeValue() const
243
{
244
	return 1.0f; // fully opaque
245
}
246
247
void OSDWidget::invalidateRecursive()
248
{
249
	invalidateLocal();
250
	invalidateChildren();
251
}
252
253
void OSDWidget::invalidateChildren() const
254
{
255
	for (auto& s : subWidgets) {
256
		s->invalidateRecursive();
257
	}
258
}
259
260
bool OSDWidget::needSuppressErrors() const
261
{
262
	if (suppressErrors) return true;
263
	if (const auto* p = getParent()) {
264
		return p->needSuppressErrors();
265
	}
266
	return false;
267
}
268
269
void OSDWidget::paintRecursive(OutputSurface& output)
270
{
271
	paint(output);
272
273
	std::optional<GLScopedClip> scopedClip;
274
	if (clip) {
275
		auto [clipPos, size] = getBoundingBox(output);
276
		scopedClip.emplace(output, clipPos, size);
277
	}
278
279
	for (const auto& s : subWidgets) {
280
		s->paintRecursive(output);
281
	}
282
}
283
284
int OSDWidget::getScaleFactor(const OutputSurface& output) const
285
{
286
	if (scaled) {
287
		return output.getLogicalWidth() / 320;
288
	} else if (getParent()) {
289
		return getParent()->getScaleFactor(output);
290
	} else {
291
		return 1;
292
	}
293
}
294
295
vec2 OSDWidget::transformPos(const OutputSurface& output,
296
                             vec2 trPos, vec2 trRelPos) const
297
{
298
	vec2 out = trPos
299
	         + (float(getScaleFactor(output)) * getPos())
300
		 + (trRelPos * getSize(output));
301
	if (const auto* p = getParent()) {
302
		out = p->transformPos(output, out, getRelPos());
303
	}
304
	return out;
305
}
306
307
vec2 OSDWidget::transformReverse(const OutputSurface& output, vec2 trPos) const
308
{
309
	if (const auto* p = getParent()) {
310
		trPos = p->transformReverse(output, trPos);
311
		return trPos
312
		       - (getRelPos() * p->getSize(output))
313
		       - (getPos() * float(getScaleFactor(output)));
314
	} else {
315
		return trPos;
316
	}
317
}
318
319
vec2 OSDWidget::getMouseCoord() const
320
{
321
	auto& videoSystem = getDisplay().getVideoSystem();
322
	if (!videoSystem.getCursorEnabled()) {
323
		// Host cursor is not visible. Return dummy mouse coords for
324
		// the OSD cursor position.
325
		// The reason for doing this is that otherwise (e.g. when using
326
		// the mouse in an MSX program) it's possible to accidentally
327
		// click on the reverse bar. This will also block the OSD mouse
328
		// in other Tcl scripts (e.g. vampier's nemesis script), but
329
		// almost always those scripts will also not be useful when the
330
		// host mouse cursor is not visible.
331
		//
332
		// We need to return coordinates that lay outside any
333
		// reasonable range. Initially we returned (NaN, NaN). But for
334
		// some reason that didn't work on dingoo: Dingoo uses
335
		// softfloat, in c++ NaN seems to behave as expected, but maybe
336
		// there's a problem on the tcl side? Anyway, when we return
337
		// +inf instead of NaN it does work.
338
		return vec2(std::numeric_limits<float>::infinity());
339
	}
340
341
	const auto* output = getDisplay().getOutputSurface();
342
	if (!output) {
343
		throw CommandException(
344
			"Can't get mouse coordinates: no window visible");
345
	}
346
	auto mouse = videoSystem.getMouseCoord();
347
	if (!mouse) {
348
		return vec2(std::numeric_limits<float>::infinity());
349
	}
350
351
	vec2 out = transformReverse(*output, vec2(*mouse));
352
	vec2 size = getSize(*output);
353
	if ((size.x == 0.0f) || (size.y == 0.0f)) {
354
		throw CommandException(
355
			"-can't get mouse coordinates: "
356
			"widget has zero width or height");
357
	}
358
	return out / size;
359
}
360
361
OSDWidget::BoundingBox OSDWidget::getBoundingBox(const OutputSurface& output) const
362
{
363
	vec2 topLeft     = transformPos(output, vec2(), vec2(0.0f));
364
	vec2 bottomRight = transformPos(output, vec2(), vec2(1.0f));
365
	return {.pos = topLeft, .size = bottomRight - topLeft};
366
}
367
368
} // namespace openmsx