💿🐜 Antkeeper source code https://antkeeper.com
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

478 lines
13 KiB

  1. /*
  2. * Copyright (C) 2021 Christopher J. Howard
  3. *
  4. * This file is part of Antkeeper source code.
  5. *
  6. * Antkeeper source code is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU General Public License as published by
  8. * the Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * Antkeeper source code is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public License
  17. * along with Antkeeper source code. If not, see <http://www.gnu.org/licenses/>.
  18. */
  19. #include "game/menu.hpp"
  20. #include "scene/text.hpp"
  21. #include "application.hpp"
  22. #include "animation/animation.hpp"
  23. #include "animation/animator.hpp"
  24. #include "animation/ease.hpp"
  25. #include "config.hpp"
  26. #include <algorithm>
  27. namespace game {
  28. namespace menu {
  29. void init_menu_item_index(game::context& ctx, const std::string& menu_name)
  30. {
  31. if (auto it = ctx.menu_item_indices.find(menu_name); it != ctx.menu_item_indices.end())
  32. {
  33. ctx.menu_item_index = &it->second;
  34. }
  35. else
  36. {
  37. ctx.menu_item_index = &ctx.menu_item_indices[menu_name];
  38. *ctx.menu_item_index = 0;
  39. }
  40. }
  41. void update_text_font(game::context& ctx)
  42. {
  43. for (auto [name, value]: ctx.menu_item_texts)
  44. {
  45. name->set_material(&ctx.menu_font_material);
  46. name->set_font(&ctx.menu_font);
  47. if (value)
  48. {
  49. value->set_material(&ctx.menu_font_material);
  50. value->set_font(&ctx.menu_font);
  51. }
  52. }
  53. }
  54. void update_text_color(game::context& ctx)
  55. {
  56. for (std::size_t i = 0; i < ctx.menu_item_texts.size(); ++i)
  57. {
  58. auto [name, value] = ctx.menu_item_texts[i];
  59. const float4& color = (i == *ctx.menu_item_index) ? config::menu_active_color : config::menu_inactive_color;
  60. name->set_color(color);
  61. if (value)
  62. value->set_color(color);
  63. }
  64. }
  65. void update_text_tweens(game::context& ctx)
  66. {
  67. for (auto [name, value]: ctx.menu_item_texts)
  68. {
  69. name->update_tweens();
  70. if (value)
  71. value->update_tweens();
  72. }
  73. }
  74. void align_text(game::context& ctx, bool center, bool has_back, float anchor_y)
  75. {
  76. // Calculate menu width
  77. float menu_width = 0.0f;
  78. float menu_spacing = ctx.menu_font.get_glyph_metrics(U'M').width;
  79. for (auto [name, value]: ctx.menu_item_texts)
  80. {
  81. float row_width = 0.0f;
  82. // Add name width to width
  83. const auto& name_bounds = static_cast<const geom::aabb<float>&>(name->get_local_bounds());
  84. row_width += name_bounds.max_point.x - name_bounds.min_point.x;
  85. if (value)
  86. {
  87. // Add value width to width
  88. //const auto& value_bounds = static_cast<const geom::aabb<float>&>(value->get_local_bounds());
  89. //row_width += value_bounds.max_point.x - value_bounds.min_point.x;
  90. // Add spacing to row width
  91. row_width += menu_spacing * 8.0f;
  92. }
  93. menu_width = std::max<float>(menu_width, row_width);
  94. }
  95. // Align texts
  96. float menu_height;
  97. if (has_back)
  98. menu_height = (ctx.menu_item_texts.size() - 1) * ctx.menu_font.get_font_metrics().linespace - ctx.menu_font.get_font_metrics().linegap;
  99. else
  100. menu_height = ctx.menu_item_texts.size() * ctx.menu_font.get_font_metrics().linespace - ctx.menu_font.get_font_metrics().linegap;
  101. float menu_x = -menu_width * 0.5f;
  102. float menu_y = anchor_y + menu_height * 0.5f - ctx.menu_font.get_font_metrics().size;
  103. for (std::size_t i = 0; i < ctx.menu_item_texts.size(); ++i)
  104. {
  105. auto [name, value] = ctx.menu_item_texts[i];
  106. float x = menu_x;
  107. float y = menu_y - ctx.menu_font.get_font_metrics().linespace * i;
  108. if (has_back && i == ctx.menu_item_texts.size() - 1)
  109. y -= ctx.menu_font.get_font_metrics().linespace;
  110. if (center || i == ctx.menu_item_texts.size() - 1)
  111. {
  112. const auto& name_bounds = static_cast<const geom::aabb<float>&>(name->get_local_bounds());
  113. const float name_width = name_bounds.max_point.x - name_bounds.min_point.x;
  114. x = -name_width * 0.5f;
  115. }
  116. name->set_translation({std::round(x), std::round(y), 0.0f});
  117. if (value)
  118. {
  119. const auto& value_bounds = static_cast<const geom::aabb<float>&>(value->get_local_bounds());
  120. const float value_width = value_bounds.max_point.x - value_bounds.min_point.x;
  121. if (center || i == ctx.menu_item_texts.size() - 1)
  122. x = -value_width * 0.5f;
  123. else
  124. x = menu_x + menu_width - value_width;
  125. value->set_translation({std::round(x), std::round(y), 0.0f});
  126. }
  127. }
  128. }
  129. void refresh_text(game::context& ctx)
  130. {
  131. for (auto [name, value]: ctx.menu_item_texts)
  132. {
  133. name->refresh();
  134. if (value)
  135. value->refresh();
  136. }
  137. }
  138. void add_text_to_ui(game::context& ctx)
  139. {
  140. for (auto [name, value]: ctx.menu_item_texts)
  141. {
  142. ctx.ui_scene->add_object(name);
  143. if (value)
  144. ctx.ui_scene->add_object(value);
  145. }
  146. }
  147. void remove_text_from_ui(game::context& ctx)
  148. {
  149. for (auto [name, value]: ctx.menu_item_texts)
  150. {
  151. ctx.ui_scene->remove_object(name);
  152. if (value)
  153. ctx.ui_scene->remove_object(value);
  154. }
  155. }
  156. void delete_text(game::context& ctx)
  157. {
  158. for (auto [name, value]: ctx.menu_item_texts)
  159. {
  160. delete name;
  161. if (value)
  162. delete value;
  163. }
  164. ctx.menu_item_texts.clear();
  165. }
  166. void delete_animations(game::context& ctx)
  167. {
  168. ctx.animator->remove_animation(ctx.menu_fade_animation);
  169. delete ctx.menu_fade_animation;
  170. ctx.menu_fade_animation = nullptr;
  171. }
  172. void clear_callbacks(game::context& ctx)
  173. {
  174. // Clear menu item callbacks
  175. ctx.menu_left_callbacks.clear();
  176. ctx.menu_right_callbacks.clear();
  177. ctx.menu_select_callbacks.clear();
  178. ctx.menu_back_callback = nullptr;
  179. }
  180. void setup_animations(game::context& ctx)
  181. {
  182. ctx.menu_fade_animation = new animation<float>();
  183. animation_channel<float>* opacity_channel = ctx.menu_fade_animation->add_channel(0);
  184. ctx.menu_fade_animation->set_frame_callback
  185. (
  186. [&ctx](int channel, const float& opacity)
  187. {
  188. for (std::size_t i = 0; i < ctx.menu_item_texts.size(); ++i)
  189. {
  190. auto [name, value] = ctx.menu_item_texts[i];
  191. float4 color = (i == *ctx.menu_item_index) ? config::menu_active_color : config::menu_inactive_color;
  192. color[3] = color[3] * opacity;
  193. if (name)
  194. name->set_color(color);
  195. if (value)
  196. value->set_color(color);
  197. }
  198. }
  199. );
  200. ctx.animator->add_animation(ctx.menu_fade_animation);
  201. }
  202. void fade_in(game::context& ctx, const std::function<void()>& end_callback)
  203. {
  204. ctx.menu_fade_animation->set_interpolator(ease<float>::out_cubic);
  205. animation_channel<float>* opacity_channel = ctx.menu_fade_animation->get_channel(0);
  206. opacity_channel->remove_keyframes();
  207. opacity_channel->insert_keyframe({0.0, 0.0f});
  208. opacity_channel->insert_keyframe({config::menu_fade_in_duration, 1.0f});
  209. ctx.menu_fade_animation->set_end_callback(end_callback);
  210. for (std::size_t i = 0; i < ctx.menu_item_texts.size(); ++i)
  211. {
  212. auto [name, value] = ctx.menu_item_texts[i];
  213. float4 color = (i == *ctx.menu_item_index) ? config::menu_active_color : config::menu_inactive_color;
  214. color[3] = 0.0f;
  215. if (name)
  216. {
  217. name->set_color(color);
  218. name->update_tweens();
  219. }
  220. if (value)
  221. {
  222. value->set_color(color);
  223. value->update_tweens();
  224. }
  225. }
  226. ctx.menu_fade_animation->stop();
  227. ctx.menu_fade_animation->play();
  228. }
  229. void fade_out(game::context& ctx, const std::function<void()>& end_callback)
  230. {
  231. ctx.menu_fade_animation->set_interpolator(ease<float>::out_cubic);
  232. animation_channel<float>* opacity_channel = ctx.menu_fade_animation->get_channel(0);
  233. opacity_channel->remove_keyframes();
  234. opacity_channel->insert_keyframe({0.0, 1.0f});
  235. opacity_channel->insert_keyframe({config::menu_fade_out_duration, 0.0f});
  236. ctx.menu_fade_animation->set_end_callback(end_callback);
  237. ctx.menu_fade_animation->stop();
  238. ctx.menu_fade_animation->play();
  239. }
  240. void fade_in_bg(game::context& ctx)
  241. {
  242. ctx.menu_bg_fade_out_animation->stop();
  243. ctx.menu_bg_fade_in_animation->stop();
  244. ctx.menu_bg_fade_in_animation->play();
  245. }
  246. void fade_out_bg(game::context& ctx)
  247. {
  248. ctx.menu_bg_fade_in_animation->stop();
  249. ctx.menu_bg_fade_out_animation->stop();
  250. ctx.menu_bg_fade_out_animation->play();
  251. }
  252. void setup_controls(game::context& ctx)
  253. {
  254. ctx.controls["menu_up"]->set_activated_callback
  255. (
  256. [&ctx]()
  257. {
  258. --(*ctx.menu_item_index);
  259. if (*ctx.menu_item_index < 0)
  260. *ctx.menu_item_index = ctx.menu_item_texts.size() - 1;
  261. update_text_color(ctx);
  262. }
  263. );
  264. ctx.controls["menu_down"]->set_activated_callback
  265. (
  266. [&ctx]()
  267. {
  268. ++(*ctx.menu_item_index);
  269. if (*ctx.menu_item_index >= ctx.menu_item_texts.size())
  270. *ctx.menu_item_index = 0;
  271. update_text_color(ctx);
  272. }
  273. );
  274. ctx.controls["menu_left"]->set_activated_callback
  275. (
  276. [&ctx]()
  277. {
  278. auto callback = ctx.menu_left_callbacks[*ctx.menu_item_index];
  279. if (callback != nullptr)
  280. callback();
  281. }
  282. );
  283. ctx.controls["menu_right"]->set_activated_callback
  284. (
  285. [&ctx]()
  286. {
  287. auto callback = ctx.menu_right_callbacks[*ctx.menu_item_index];
  288. if (callback != nullptr)
  289. callback();
  290. }
  291. );
  292. ctx.controls["menu_select"]->set_activated_callback
  293. (
  294. [&ctx]()
  295. {
  296. auto callback = ctx.menu_select_callbacks[*ctx.menu_item_index];
  297. if (callback != nullptr)
  298. callback();
  299. }
  300. );
  301. ctx.controls["menu_back"]->set_activated_callback
  302. (
  303. [&ctx]()
  304. {
  305. if (ctx.menu_back_callback != nullptr)
  306. ctx.menu_back_callback();
  307. }
  308. );
  309. ctx.menu_mouse_tracker->set_mouse_moved_callback
  310. (
  311. [&ctx](const mouse_moved_event& event)
  312. {
  313. const float padding = config::menu_mouseover_padding * ctx.menu_font.get_font_metrics().size;
  314. for (std::size_t i = 0; i < ctx.menu_item_texts.size(); ++i)
  315. {
  316. auto [name, value] = ctx.menu_item_texts[i];
  317. const auto& name_bounds = static_cast<const geom::aabb<float>&>(name->get_world_bounds());
  318. float min_x = name_bounds.min_point.x;
  319. float min_y = name_bounds.min_point.y;
  320. float max_x = name_bounds.max_point.x;
  321. float max_y = name_bounds.max_point.y;
  322. if (value)
  323. {
  324. const auto& value_bounds = static_cast<const geom::aabb<float>&>(value->get_world_bounds());
  325. min_x = std::min<float>(min_x, value_bounds.min_point.x);
  326. min_y = std::min<float>(min_y, value_bounds.min_point.y);
  327. max_x = std::max<float>(max_x, value_bounds.max_point.x);
  328. max_y = std::max<float>(max_y, value_bounds.max_point.y);
  329. }
  330. min_x -= padding;
  331. min_y -= padding;
  332. max_x += padding;
  333. max_y += padding;
  334. const auto& viewport = ctx.app->get_viewport_dimensions();
  335. const float x = static_cast<float>(event.x - viewport[0] / 2);
  336. const float y = static_cast<float>((viewport[1] - event.y + 1) - viewport[1] / 2);
  337. if (x >= min_x && x <= max_x)
  338. {
  339. if (y >= min_y && y <= max_y)
  340. {
  341. *ctx.menu_item_index = i;
  342. update_text_color(ctx);
  343. break;
  344. }
  345. }
  346. }
  347. }
  348. );
  349. ctx.menu_mouse_tracker->set_mouse_button_pressed_callback
  350. (
  351. [&ctx](const mouse_button_pressed_event& event)
  352. {
  353. const float padding = config::menu_mouseover_padding * ctx.menu_font.get_font_metrics().size;
  354. for (std::size_t i = 0; i < ctx.menu_item_texts.size(); ++i)
  355. {
  356. auto [name, value] = ctx.menu_item_texts[i];
  357. const auto& name_bounds = static_cast<const geom::aabb<float>&>(name->get_world_bounds());
  358. float min_x = name_bounds.min_point.x;
  359. float min_y = name_bounds.min_point.y;
  360. float max_x = name_bounds.max_point.x;
  361. float max_y = name_bounds.max_point.y;
  362. if (value)
  363. {
  364. const auto& value_bounds = static_cast<const geom::aabb<float>&>(value->get_world_bounds());
  365. min_x = std::min<float>(min_x, value_bounds.min_point.x);
  366. min_y = std::min<float>(min_y, value_bounds.min_point.y);
  367. max_x = std::max<float>(max_x, value_bounds.max_point.x);
  368. max_y = std::max<float>(max_y, value_bounds.max_point.y);
  369. }
  370. min_x -= padding;
  371. min_y -= padding;
  372. max_x += padding;
  373. max_y += padding;
  374. const auto& viewport = ctx.app->get_viewport_dimensions();
  375. const float x = static_cast<float>(event.x - viewport[0] / 2);
  376. const float y = static_cast<float>((viewport[1] - event.y + 1) - viewport[1] / 2);
  377. if (x >= min_x && x <= max_x)
  378. {
  379. if (y >= min_y && y <= max_y)
  380. {
  381. *ctx.menu_item_index = i;
  382. update_text_color(ctx);
  383. if (event.button == 1)
  384. {
  385. auto callback = ctx.menu_select_callbacks[i];
  386. if (callback)
  387. callback();
  388. }
  389. else if (event.button == 3)
  390. {
  391. auto callback = ctx.menu_left_callbacks[i];
  392. if (callback)
  393. callback();
  394. }
  395. return;
  396. }
  397. }
  398. }
  399. }
  400. );
  401. }
  402. void clear_controls(game::context& ctx)
  403. {
  404. ctx.controls["menu_up"]->set_activated_callback(nullptr);
  405. ctx.controls["menu_down"]->set_activated_callback(nullptr);
  406. ctx.controls["menu_left"]->set_activated_callback(nullptr);
  407. ctx.controls["menu_right"]->set_activated_callback(nullptr);
  408. ctx.controls["menu_select"]->set_activated_callback(nullptr);
  409. ctx.controls["menu_back"]->set_activated_callback(nullptr);
  410. ctx.menu_mouse_tracker->set_mouse_moved_callback(nullptr);
  411. ctx.menu_mouse_tracker->set_mouse_button_pressed_callback(nullptr);
  412. }
  413. } // namespace menu
  414. } // namespace game