| 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 |
|
| 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; |
| 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; |
| 68 |
|
| 69 |
|
| 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 |
|
| 109 |
|
| 110 |
|
| 111 |
|
| 112 |
|
| 113 |
|
| 114 |
|
| 115 |
|
| 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 |
|
| 137 |
auto it1 = begin(subWidgets); |
| 138 |
while (it1->get() != elem) ++it1; |
| 139 |
|
| 140 |
float elemZ = elem->getZ(); |
| 141 |
auto it2 = it1; |
| 142 |
++it2; |
| 143 |
while ((it2 != end(subWidgets)) && ((*it2)->getZ() < elemZ)) ++it2; |
| 144 |
|
| 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 |
|
| 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 |
|
| 160 |
auto it2 = it1; |
| 161 |
if ((it2 != begin(subWidgets)) && ((it2 - 1)->get() == elem)) return; |
| 162 |
while (it2->get() != elem) ++it2; |
| 163 |
|
| 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; |
| 185 |
z = z2; |
| 186 |
if (auto* p = getParent()) { |
| 187 |
|
| 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; |
| 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 |
|
| 324 |
|
| 325 |
|
| 326 |
|
| 327 |
|
| 328 |
|
| 329 |
|
| 330 |
|
| 331 |
|
| 332 |
|
| 333 |
|
| 334 |
|
| 335 |
|
| 336 |
|
| 337 |
|
| 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 |
} |