ScriptableObjectAdapter.cpp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521
  1. #include "ScriptableObjectAdapter.h"
  2. #include <algorithm>
  3. #include <cstring>
  4. #include <utility>
  5. #include "WallpaperEngine/Data/Model/DynamicValue.h"
  6. #include "WallpaperEngine/Data/Model/Object.h"
  7. #include "WallpaperEngine/Data/Utils/ScopeGuard.h"
  8. #include "WallpaperEngine/Logging/Log.h"
  9. #include "WallpaperEngine/Render/Objects/CImage.h"
  10. #include "WallpaperEngine/VideoPlayback/MPV/GLPlayer.h"
  11. #include "WallpaperEngine/Scripting/ScriptEngine.h"
  12. #include "WallpaperEngine/Scripting/ScriptableObject.h"
  13. using namespace WallpaperEngine::Data::Model;
  14. using namespace WallpaperEngine::Data::Utils;
  15. using namespace WallpaperEngine::Scripting::Adapters;
  16. #define SCRIPTABLE_OPAQUE_MAGIC 0xdeadbeef
  17. struct OpaqueScriptableObjectAdapter {
  18. unsigned int magic;
  19. ScriptableObjectAdapter& adapter;
  20. WallpaperEngine::Scripting::ScriptableObject& object;
  21. };
  22. // the object's address rides along as function data, safe because every ScriptableObject outlives the script context
  23. JSValue scriptableobject_playback_call (
  24. JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValueConst* func_data
  25. ) {
  26. int64_t address = 0;
  27. JS_ToInt64 (ctx, &address, func_data[0]);
  28. auto* object = reinterpret_cast<WallpaperEngine::Scripting::ScriptableObject*> (static_cast<intptr_t> (address));
  29. if (magic == 2) {
  30. return JS_NewBool (ctx, object->isPlaying ());
  31. }
  32. using Playback = WallpaperEngine::Scripting::ScriptableObject::Playback;
  33. object->setPlayback (magic == 0 ? Playback::Playing : magic == 1 ? Playback::Paused : Playback::Stopped);
  34. return JS_UNDEFINED;
  35. }
  36. // only scriptable layers can be handed to scripts, a plain group parent comes back as null
  37. JSValue scriptableobject_hierarchy_call (
  38. JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValueConst* func_data
  39. ) {
  40. int64_t objectAddress = 0;
  41. int64_t engineAddress = 0;
  42. JS_ToInt64 (ctx, &objectAddress, func_data[0]);
  43. JS_ToInt64 (ctx, &engineAddress, func_data[1]);
  44. auto* object = reinterpret_cast<WallpaperEngine::Scripting::ScriptableObject*> (static_cast<intptr_t> (objectAddress));
  45. auto* engine = reinterpret_cast<WallpaperEngine::Scripting::ScriptEngine*> (static_cast<intptr_t> (engineAddress));
  46. const auto& scene = engine->getScene ();
  47. if (magic == 0) {
  48. const auto& parentId = object->getObject ().parent;
  49. if (!parentId.has_value ()) {
  50. return JS_NULL;
  51. }
  52. const auto* parent = scene.getObject (*parentId);
  53. if (parent == nullptr || !parent->is<WallpaperEngine::Scripting::ScriptableObject> ()) {
  54. return JS_NULL;
  55. }
  56. return engine->getAdapters ().object->instantiate (
  57. const_cast<WallpaperEngine::Scripting::ScriptableObject&> (
  58. *parent->as<WallpaperEngine::Scripting::ScriptableObject> ()
  59. )
  60. );
  61. }
  62. JSValue children = JS_NewArray (ctx);
  63. uint32_t index = 0;
  64. for (const auto* candidate : scene.getObjectsByRenderOrder ()) {
  65. const auto& candidateParent = candidate->getObject ().parent;
  66. if (!candidateParent.has_value () || *candidateParent != object->getObject ().id
  67. || !candidate->is<WallpaperEngine::Scripting::ScriptableObject> ()) {
  68. continue;
  69. }
  70. JS_SetPropertyUint32 (
  71. ctx, children, index++,
  72. engine->getAdapters ().object->instantiate (
  73. const_cast<WallpaperEngine::Scripting::ScriptableObject&> (
  74. *candidate->as<WallpaperEngine::Scripting::ScriptableObject> ()
  75. )
  76. )
  77. );
  78. }
  79. return children;
  80. }
  81. // effect handles keep the effect's address, safe for the same reason as the object address above
  82. JSValue scriptableeffect_visible_call (
  83. JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValueConst* func_data
  84. ) {
  85. int64_t effectAddress = 0;
  86. int64_t engineAddress = 0;
  87. JS_ToInt64 (ctx, &effectAddress, func_data[0]);
  88. JS_ToInt64 (ctx, &engineAddress, func_data[1]);
  89. auto* effect = reinterpret_cast<ImageEffect*> (static_cast<intptr_t> (effectAddress));
  90. auto* engine = reinterpret_cast<WallpaperEngine::Scripting::ScriptEngine*> (static_cast<intptr_t> (engineAddress));
  91. if (magic == 0) {
  92. return JS_NewBool (ctx, effect->visible->value->getBool ());
  93. }
  94. if (argc > 0) {
  95. engine->assignJsValue (argv[0], *effect->visible->value);
  96. }
  97. return JS_UNDEFINED;
  98. }
  99. const std::vector<ImageEffectUniquePtr>* effectsOf (const Object& object) {
  100. if (object.is<Image> ()) {
  101. return &object.as<Image> ()->effects;
  102. }
  103. if (object.is<Text> ()) {
  104. return &object.as<Text> ()->effects;
  105. }
  106. return nullptr;
  107. }
  108. JSValue scriptableeffect_instantiate (JSContext* ctx, ImageEffect& effect, WallpaperEngine::Scripting::ScriptEngine& engine) {
  109. JSValue handle = JS_NewObject (ctx);
  110. JSValue data[] = {
  111. JS_NewInt64 (ctx, static_cast<int64_t> (reinterpret_cast<intptr_t> (&effect))),
  112. JS_NewInt64 (ctx, static_cast<int64_t> (reinterpret_cast<intptr_t> (&engine))),
  113. };
  114. const JSAtom visible = JS_NewAtom (ctx, "visible");
  115. JS_DefinePropertyGetSet (
  116. ctx, handle, visible, JS_NewCFunctionData (ctx, scriptableeffect_visible_call, 0, 0, 2, data),
  117. JS_NewCFunctionData (ctx, scriptableeffect_visible_call, 1, 1, 2, data), JS_PROP_ENUMERABLE
  118. );
  119. JS_FreeAtom (ctx, visible);
  120. JS_SetPropertyStr (ctx, handle, "name", JS_NewString (ctx, effect.name.c_str ()));
  121. return handle;
  122. }
  123. JSValue scriptableobject_effect_call (
  124. JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValueConst* func_data
  125. ) {
  126. int64_t objectAddress = 0;
  127. int64_t engineAddress = 0;
  128. JS_ToInt64 (ctx, &objectAddress, func_data[0]);
  129. JS_ToInt64 (ctx, &engineAddress, func_data[1]);
  130. auto* object = reinterpret_cast<WallpaperEngine::Scripting::ScriptableObject*> (static_cast<intptr_t> (objectAddress));
  131. auto* engine = reinterpret_cast<WallpaperEngine::Scripting::ScriptEngine*> (static_cast<intptr_t> (engineAddress));
  132. const auto* effects = effectsOf (object->getObject ());
  133. if (magic == 0) {
  134. return JS_NewInt32 (ctx, effects == nullptr ? 0 : static_cast<int> (effects->size ()));
  135. }
  136. if (effects == nullptr || argc < 1) {
  137. return JS_UNDEFINED;
  138. }
  139. if (JS_IsNumber (argv[0])) {
  140. int index = 0;
  141. JS_ToInt32 (ctx, &index, argv[0]);
  142. if (index < 0 || static_cast<size_t> (index) >= effects->size ()) {
  143. return JS_UNDEFINED;
  144. }
  145. return scriptableeffect_instantiate (ctx, *(*effects)[index], *engine);
  146. }
  147. const char* name = JS_ToCString (ctx, argv[0]);
  148. if (name == nullptr) {
  149. return JS_UNDEFINED;
  150. }
  151. ScopeGuard guard ([=] { JS_FreeCString (ctx, name); });
  152. for (const auto& effect : *effects) {
  153. if (effect->name == name) {
  154. return scriptableeffect_instantiate (ctx, *effect, *engine);
  155. }
  156. }
  157. return JS_UNDEFINED;
  158. }
  159. // video textures are only ever reached through the player's address, kept alive by the layer's texture for as long as the scene exists
  160. JSValue videotexture_call (
  161. JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValueConst* func_data
  162. ) {
  163. int64_t playerAddress = 0;
  164. int64_t engineAddress = 0;
  165. JS_ToInt64 (ctx, &playerAddress, func_data[0]);
  166. JS_ToInt64 (ctx, &engineAddress, func_data[1]);
  167. auto* player = reinterpret_cast<WallpaperEngine::VideoPlayback::MPV::GLPlayer*> (static_cast<intptr_t> (playerAddress));
  168. auto* engine = reinterpret_cast<WallpaperEngine::Scripting::ScriptEngine*> (static_cast<intptr_t> (engineAddress));
  169. switch (magic) {
  170. case 0:
  171. if (player->hasEnded ()) {
  172. player->seek (0.0);
  173. }
  174. player->clearPaused ();
  175. return JS_UNDEFINED;
  176. case 1:
  177. player->setPaused ();
  178. return JS_UNDEFINED;
  179. case 2:
  180. player->setPaused ();
  181. player->seek (0.0);
  182. return JS_UNDEFINED;
  183. case 3:
  184. return JS_NewBool (ctx, !player->isPaused ());
  185. case 4:
  186. return JS_NewBool (ctx, player->isPaused ());
  187. case 5:
  188. return JS_NewBool (ctx, player->isPaused () && player->getPlaybackPosition () < 0.001);
  189. case 6:
  190. return JS_NewFloat64 (ctx, player->getPlaybackPosition ());
  191. case 7: {
  192. double seconds = 0.0;
  193. if (argc > 0 && JS_ToFloat64 (ctx, &seconds, argv[0]) == 0) {
  194. player->seek (std::max (seconds, 0.0));
  195. }
  196. return JS_UNDEFINED;
  197. }
  198. case 8:
  199. if (argc > 0) {
  200. engine->addVideoEndedCallback (player, argv[0]);
  201. }
  202. return JS_UNDEFINED;
  203. case 9:
  204. return JS_NewBool (ctx, player->isLooping ());
  205. case 10:
  206. if (argc > 0) {
  207. player->setLoop (JS_ToBool (ctx, argv[0]) > 0);
  208. }
  209. return JS_UNDEFINED;
  210. case 11:
  211. return JS_NewFloat64 (ctx, player->getDuration ());
  212. case 12:
  213. return JS_NewFloat64 (ctx, player->getSpeed ());
  214. case 13: {
  215. double rate = 0.0;
  216. if (argc > 0 && JS_ToFloat64 (ctx, &rate, argv[0]) == 0 && rate > 0.0) {
  217. player->setSpeed (rate);
  218. }
  219. return JS_UNDEFINED;
  220. }
  221. default:
  222. return JS_UNDEFINED;
  223. }
  224. }
  225. JSValue videotexture_instantiate (
  226. JSContext* ctx, WallpaperEngine::VideoPlayback::MPV::GLPlayer& player, WallpaperEngine::Scripting::ScriptEngine& engine
  227. ) {
  228. JSValue handle = JS_NewObject (ctx);
  229. JSValue data[] = {
  230. JS_NewInt64 (ctx, static_cast<int64_t> (reinterpret_cast<intptr_t> (&player))),
  231. JS_NewInt64 (ctx, static_cast<int64_t> (reinterpret_cast<intptr_t> (&engine))),
  232. };
  233. static constexpr struct {
  234. const char* name;
  235. int magic;
  236. int length;
  237. } calls[] = { { "play", 0, 0 }, { "pause", 1, 0 }, { "stop", 2, 0 },
  238. { "isPlaying", 3, 0 }, { "isPaused", 4, 0 }, { "isStopped", 5, 0 },
  239. { "getCurrentTime", 6, 0 }, { "setCurrentTime", 7, 1 }, { "addEndedCallback", 8, 1 } };
  240. for (const auto& call : calls) {
  241. JS_SetPropertyStr (
  242. ctx, handle, call.name, JS_NewCFunctionData (ctx, videotexture_call, call.length, call.magic, 2, data)
  243. );
  244. }
  245. static constexpr struct {
  246. const char* name;
  247. int getter;
  248. int setter;
  249. } properties[] = { { "loop", 9, 10 }, { "duration", 11, 14 }, { "rate", 12, 13 } };
  250. // "duration" gets an ignoring setter (14) since strict mode scripts throw on a property with none
  251. for (const auto& property : properties) {
  252. const JSAtom atom = JS_NewAtom (ctx, property.name);
  253. JS_DefinePropertyGetSet (
  254. ctx, handle, atom, JS_NewCFunctionData (ctx, videotexture_call, 0, property.getter, 2, data),
  255. JS_NewCFunctionData (ctx, videotexture_call, 1, property.setter, 2, data), JS_PROP_ENUMERABLE
  256. );
  257. JS_FreeAtom (ctx, atom);
  258. }
  259. return handle;
  260. }
  261. // scripts call this on image layers whose material is a video (mp4 texture), everything else gets null
  262. JSValue scriptableobject_video_texture_call (
  263. JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValueConst* func_data
  264. ) {
  265. int64_t objectAddress = 0;
  266. int64_t engineAddress = 0;
  267. JS_ToInt64 (ctx, &objectAddress, func_data[0]);
  268. JS_ToInt64 (ctx, &engineAddress, func_data[1]);
  269. auto* object = reinterpret_cast<WallpaperEngine::Scripting::ScriptableObject*> (static_cast<intptr_t> (objectAddress));
  270. auto* engine = reinterpret_cast<WallpaperEngine::Scripting::ScriptEngine*> (static_cast<intptr_t> (engineAddress));
  271. if (!object->is<WallpaperEngine::Render::Objects::CImage> ()) {
  272. return JS_NULL;
  273. }
  274. const auto texture = object->as<WallpaperEngine::Render::Objects::CImage> ()->getTexture ();
  275. if (texture == nullptr || texture->getPlayer () == nullptr) {
  276. return JS_NULL;
  277. }
  278. return videotexture_instantiate (ctx, *texture->getPlayer (), *engine);
  279. }
  280. JSValue scriptableobject_property_get (JSContext* ctx, JSValueConst obj_val, JSAtom atom, JSValueConst receiver) {
  281. JSClassID classId = 0;
  282. auto* container = static_cast<OpaqueScriptableObjectAdapter*> (JS_GetAnyOpaque (obj_val, &classId));
  283. if (!container || container->magic != SCRIPTABLE_OPAQUE_MAGIC) {
  284. return JS_EXCEPTION;
  285. }
  286. const char* name = JS_AtomToCString (ctx, atom);
  287. if (name == nullptr) {
  288. return JS_EXCEPTION;
  289. }
  290. ScopeGuard guard ([=] { JS_FreeCString (ctx, name); });
  291. if (auto* property = container->object.tryGetProperty (name); property != nullptr) {
  292. return container->adapter.getEngine ().dynamicToJs (*property);
  293. }
  294. static constexpr struct {
  295. const char* name;
  296. int magic;
  297. } playbackCalls[] = { { "play", 0 }, { "pause", 1 }, { "stop", 3 }, { "isPlaying", 2 } };
  298. for (const auto& call : playbackCalls) {
  299. if (std::strcmp (name, call.name) == 0) {
  300. JSValue address[]
  301. = { JS_NewInt64 (ctx, static_cast<int64_t> (reinterpret_cast<intptr_t> (&container->object))) };
  302. return JS_NewCFunctionData (ctx, scriptableobject_playback_call, 0, call.magic, 1, address);
  303. }
  304. }
  305. static constexpr struct {
  306. const char* name;
  307. int magic;
  308. } hierarchyCalls[] = { { "getParent", 0 }, { "getChildren", 1 } };
  309. for (const auto& call : hierarchyCalls) {
  310. if (std::strcmp (name, call.name) == 0) {
  311. JSValue data[] = {
  312. JS_NewInt64 (ctx, static_cast<int64_t> (reinterpret_cast<intptr_t> (&container->object))),
  313. JS_NewInt64 (ctx, static_cast<int64_t> (reinterpret_cast<intptr_t> (&container->adapter.getEngine ()))),
  314. };
  315. return JS_NewCFunctionData (ctx, scriptableobject_hierarchy_call, 0, call.magic, 2, data);
  316. }
  317. }
  318. static constexpr struct {
  319. const char* name;
  320. int magic;
  321. } effectCalls[] = { { "getEffectCount", 0 }, { "getEffect", 1 } };
  322. for (const auto& call : effectCalls) {
  323. if (std::strcmp (name, call.name) == 0) {
  324. JSValue data[] = {
  325. JS_NewInt64 (ctx, static_cast<int64_t> (reinterpret_cast<intptr_t> (&container->object))),
  326. JS_NewInt64 (ctx, static_cast<int64_t> (reinterpret_cast<intptr_t> (&container->adapter.getEngine ()))),
  327. };
  328. return JS_NewCFunctionData (ctx, scriptableobject_effect_call, 1, call.magic, 2, data);
  329. }
  330. }
  331. if (std::strcmp (name, "getVideoTexture") == 0) {
  332. JSValue data[] = {
  333. JS_NewInt64 (ctx, static_cast<int64_t> (reinterpret_cast<intptr_t> (&container->object))),
  334. JS_NewInt64 (ctx, static_cast<int64_t> (reinterpret_cast<intptr_t> (&container->adapter.getEngine ()))),
  335. };
  336. return JS_NewCFunctionData (ctx, scriptableobject_video_texture_call, 0, 0, 2, data);
  337. }
  338. if (std::strcmp (name, "name") == 0) {
  339. return JS_NewString (ctx, container->object.getObject ().name.c_str ());
  340. }
  341. if (std::strcmp (name, "id") == 0) {
  342. return JS_NewInt32 (ctx, container->object.getObject ().id);
  343. }
  344. // "size" isn't a DynamicValue-backed property, but thisLayer.size is a commonly used part
  345. // of the WE scripting API, so it's special-cased here. Checked against the data model
  346. // (Image/Text) rather than the render object (CImage/CText) because scripted properties can
  347. // run their first update() from inside the base ScriptableObject constructor, before the
  348. // derived render object has finished constructing - a dynamic_cast to it at that point would
  349. // incorrectly report "not yet that type".
  350. if (std::strcmp (name, "size") == 0) {
  351. const auto& modelObject = container->object.getObject ();
  352. const glm::vec2* size = nullptr;
  353. if (modelObject.is<Image> ()) {
  354. size = &modelObject.as<Image> ()->size;
  355. } else if (modelObject.is<Text> ()) {
  356. size = &modelObject.as<Text> ()->size;
  357. }
  358. if (size != nullptr) {
  359. const DynamicValue sizeValue (*size);
  360. return container->adapter.getEngine ().getAdapters ().vec2->instantiate (
  361. const_cast<DynamicValue&> (sizeValue), true
  362. );
  363. }
  364. }
  365. return JS_UNDEFINED;
  366. }
  367. int scriptableobject_property_set (
  368. JSContext* ctx, JSValueConst obj_val, JSAtom atom, JSValueConst val, JSValueConst receiver, int flags
  369. ) {
  370. JSClassID classId = 0;
  371. auto* container = static_cast<OpaqueScriptableObjectAdapter*> (JS_GetAnyOpaque (obj_val, &classId));
  372. if (!container || container->magic != SCRIPTABLE_OPAQUE_MAGIC) {
  373. return -1;
  374. }
  375. const char* name = JS_AtomToCString (ctx, atom);
  376. if (name == nullptr) {
  377. return -1;
  378. }
  379. ScopeGuard guard ([=] { JS_FreeCString (ctx, name); });
  380. // Write through to the real property so `thisLayer.visible = ...` etc. actually takes
  381. // effect. Properties not backed by a DynamicValue (e.g. "horizontalalign", plain model
  382. // strings) fall through and return 0 rather than -1: since scripts run as strict-mode ES
  383. // modules, returning -1 here would throw and abort the whole script over an unsupported
  384. // property, so silently accepting the write is the safer default.
  385. if (auto* property = container->object.tryGetProperty (name); property != nullptr) {
  386. container->adapter.getEngine ().assignJsValue (val, *property);
  387. }
  388. return 0;
  389. }
  390. ScriptableObjectAdapter::ScriptableObjectAdapter (ScriptEngine& engine, std::string name) :
  391. ObjectAdapter (engine),
  392. m_exoticMethods ({ .get_property = scriptableobject_property_get, .set_property = scriptableobject_property_set }),
  393. m_name (std::move (name)) {
  394. this->registerType (
  395. {
  396. .class_name = m_name.c_str (),
  397. .exotic = &m_exoticMethods,
  398. }
  399. );
  400. }
  401. JSValue ScriptableObjectAdapter::instantiate (ScriptableObject& object) {
  402. JSValue result = this->ObjectAdapter::instantiate (object);
  403. JS_SetOpaque (
  404. result,
  405. new OpaqueScriptableObjectAdapter { .magic = SCRIPTABLE_OPAQUE_MAGIC, .adapter = *this, .object = object }
  406. );
  407. return result;
  408. }
  409. JSValue ScriptableObjectAdapter::instantiate (DynamicValue& value) {
  410. throw std::runtime_error ("Cannot create a ScriptableObject instance from a DynamicValue");
  411. }
  412. WallpaperEngine::Scripting::ScriptableObject* ScriptableObjectAdapter::getObject (JSValueConst value) {
  413. JSClassID classId = 0;
  414. auto* container = static_cast<OpaqueScriptableObjectAdapter*> (JS_GetAnyOpaque (value, &classId));
  415. if (container == nullptr || container->magic != SCRIPTABLE_OPAQUE_MAGIC) {
  416. return nullptr;
  417. }
  418. return &container->object;
  419. }