Air-Trap 1.0.0
A multiplayer R-Type clone game engine built with C++23 and ECS architecture
Loading...
Searching...
No Matches
UISystem.cpp
Go to the documentation of this file.
1/*
2** EPITECH PROJECT, 2025
3** Air-Trap
4** File description:
5** UISystem
6*/
7
9
10#include "RType/Logger.hpp"
12#include <SFML/Window/Joystick.hpp>
13#include <cmath>
14
15namespace rtp::client
16{
17 void UISystem::update(float dt)
18 {
19 // Handle gamepad cursor movement
22
23 auto buttonsCheck =
25 if (!buttonsCheck) {
26 return;
27 }
28
29 // Use gamepad cursor if in gamepad mode, otherwise mouse
30 sf::Vector2i cursorPos = _gamepadMode
31 ? sf::Vector2i(static_cast<int>(_gamepadCursorPos.x), static_cast<int>(_gamepadCursorPos.y))
32 : sf::Mouse::getPosition(_window);
33
34 handleMouseMove(cursorPos);
35
36 buttonsCheck =
38 if (!buttonsCheck) {
39 return;
40 }
41
42 static bool wasPressed = false;
43 bool isPressed = sf::Mouse::isButtonPressed(sf::Mouse::Button::Left);
44
45 if (isPressed && !wasPressed) {
46 handleMouseClick(cursorPos);
47 }
48
49 wasPressed = isPressed;
50 }
51
52 void UISystem::handleEvent(const sf::Event &event)
53 {
54 if (const auto *te = event.getIf<sf::Event::TextEntered>()) {
55 handleTextEntered(te->unicode);
56 }
57 if (const auto *kp = event.getIf<sf::Event::KeyPressed>()) {
58 handleKeyPressed(kp->code);
59 }
60 }
61
62 void UISystem::handleMouseMove(const sf::Vector2i &mousePos)
63 {
64 auto buttonsResult =
66 if (!buttonsResult)
67 return;
68
69 auto &buttons = buttonsResult.value().get();
70 for (const auto &entity : buttons.entities()) {
71 auto &button = buttons[entity];
72
73 if (isMouseOverButton(button, mousePos)) {
75 } else {
77 }
78 }
79 }
80
81 void UISystem::handleMouseClick(const sf::Vector2i &mousePos)
82 {
83 auto dropdownsResult =
85 if (dropdownsResult) {
86 auto &dropdowns = dropdownsResult.value().get();
87
88 for (const auto &entity : dropdowns.entities()) {
89 auto &dropdown = dropdowns[entity];
90
91 if (dropdown.isOpen) {
92 int optionIndex =
93 getDropdownOptionAtMouse(dropdown, mousePos);
94 if (optionIndex >= 0) {
95 dropdown.selectedIndex = optionIndex;
96 dropdown.isOpen = false;
97
98 if (dropdown.onSelect) {
99 dropdown.onSelect(optionIndex);
100 }
101
102 log::info("Dropdown option {} selected",
103 optionIndex);
104 return;
105 }
106 }
107 }
108
109 for (const auto &entity : dropdowns.entities()) {
110 auto &dropdown = dropdowns[entity];
111
112 if (isMouseOverDropdown(dropdown, mousePos)) {
113 dropdown.isOpen = !dropdown.isOpen;
114 log::info("Dropdown toggled: {}",
115 dropdown.isOpen ? "open" : "closed");
116
117 for (const auto &otherEntity : dropdowns.entities()) {
118 if (otherEntity != entity) {
119 dropdowns[otherEntity].isOpen = false;
120 }
121 }
122 return;
123 }
124 }
125
126 bool hadOpenDropdown = false;
127 for (const auto &entity : dropdowns.entities()) {
128 if (dropdowns[entity].isOpen) {
129 dropdowns[entity].isOpen = false;
130 hadOpenDropdown = true;
131 }
132 }
133 if (hadOpenDropdown) {
134 return;
135 }
136 }
137
138 {
139 auto inputsResult =
141 if (inputsResult) {
142 auto &inputs = inputsResult.value().get();
143
145 [&](const ecs::components::ui::TextInput &in) -> bool {
146 return mousePos.x >=
147 in.position.x &&
148 mousePos.x <=
149 in.position.x +
150 in.size.x &&
151 mousePos.y >=
152 in.position.y &&
153 mousePos.y <=
154 in.position.y +
155 in.size.y;
156 };
157
158 bool clickedOnAnyInput = false;
159
160 for (const auto &entity : inputs.entities()) {
161 if (isMouseOverTextInput(inputs[entity])) {
162 clickedOnAnyInput = true;
163 break;
164 }
165 }
166
167 if (clickedOnAnyInput) {
168 for (const auto &entity : inputs.entities()) {
169 auto &in = inputs[entity];
170 const bool over = isMouseOverTextInput(in);
171 in.isFocused = over;
172
173 if (over) {
174 in.showCursor = true;
175 in.blinkTimer = 0.0f;
176 } else {
177 in.showCursor = false;
178 in.blinkTimer = 0.0f;
179 }
180 }
181
182 log::info("TextInput focused");
183 return;
184 } else {
185 bool hadFocus = false;
186 for (const auto &entity : inputs.entities()) {
187 auto &in = inputs[entity];
188 if (in.isFocused)
189 hadFocus = true;
190 in.isFocused = false;
191 in.showCursor = false;
192 in.blinkTimer = 0.0f;
193 }
194 if (hadFocus) {
195 log::info("TextInput unfocused");
196 }
197 }
198 }
199 }
200
201 auto buttonsResult =
203 if (buttonsResult) {
204 auto &buttons = buttonsResult.value().get();
205
206 std::vector<std::pair<ecs::Entity, std::function<void()>>>
207 buttonCallbacks;
208 for (const auto &entity : buttons.entities()) {
209 auto &button = buttons[entity];
210 if (!button.onClick && button.text.empty()) {
211 continue;
212 }
213 if (isMouseOverButton(button, mousePos)) {
214 button.state =
216
217 // Jouer le son de clic
219
220 if (button.onClick) {
221 buttonCallbacks.emplace_back(entity, button.onClick);
222 }
223
224 log::info("Button '{}' clicked", button.text);
225 break;
226 }
227 }
228
229 for (auto &[entity, callback] : buttonCallbacks) {
230 (void)entity;
231 callback();
232 return;
233 }
234 }
235
236 auto slidersResult =
238 if (slidersResult) {
239 auto &sliders = slidersResult.value().get();
240 for (const auto &entity : sliders.entities()) {
241 auto &slider = sliders[entity];
242
243 if (isMouseOverSlider(slider, mousePos)) {
244 slider.isDragging = true;
245 updateSliderValue(slider, mousePos);
246 log::info("Slider clicked at value: {}",
247 slider.currentValue);
248 return;
249 }
250 }
251 }
252 }
253
255 const ecs::components::ui::Button &button,
256 const sf::Vector2i &mousePos)
257 {
258 return mousePos.x >=
259 button.position.x &&
260 mousePos.x <=
261 button.position.x +
262 button.size.x &&
263 mousePos.y >=
264 button.position.y &&
265 mousePos.y <=
266 button.position.y +
267 button.size.y;
268 }
269
271 const ecs::components::ui::Slider &slider,
272 const sf::Vector2i &mousePos)
273 {
274 return mousePos.x >=
275 slider.position.x &&
276 mousePos.x <=
277 slider.position.x +
278 slider.size.x &&
279 mousePos.y >=
280 slider.position.y &&
281 mousePos.y <=
282 slider.position.y +
283 slider.size.y;
284 }
285
287 const ecs::components::ui::Dropdown &dropdown,
288 const sf::Vector2i &mousePos)
289 {
290 return mousePos.x >=
291 dropdown.position.x &&
292 mousePos.x <=
293 dropdown.position.x +
294 dropdown.size.x &&
295 mousePos.y >=
296 dropdown.position.y &&
297 mousePos.y <=
298 dropdown.position.y +
299 dropdown.size.y;
300 }
301
303 const sf::Vector2i &mousePos)
304 {
305 float relativeX = mousePos.x - slider.position.x;
306 float percentage = std::clamp(relativeX / slider.size.x, 0.0f, 1.0f);
307
308 slider.currentValue =
309 slider.minValue + percentage * (slider.maxValue - slider.minValue);
310
311 if (slider.onChange) {
312 slider.onChange(slider.currentValue);
313 }
314 }
315
317 const ecs::components::ui::Dropdown &dropdown,
318 const sf::Vector2i &mousePos)
319 {
320 float optionHeight = dropdown.size.y;
321 float startY = dropdown.position.y + dropdown.size.y;
322
323 for (size_t i = 0; i < dropdown.options.size(); ++i) {
324 float optionY = startY + i * optionHeight;
325
326 if (mousePos.x >=
327 dropdown.position.x &&
328 mousePos.x <=
329 dropdown.position.x +
330 dropdown.size.x &&
331 mousePos.y >=
332 optionY &&
333 mousePos.y <=
334 optionY +
335 optionHeight) {
336 return static_cast<int>(i);
337 }
338 }
339
340 return -1;
341 }
342
345 const sf::Vector2i &mousePos) const
346 {
347 return mousePos.x >=
348 input.position.x &&
349 mousePos.x <=
350 input.position.x +
351 input.size.x &&
352 mousePos.y >=
353 input.position.y &&
354 mousePos.y <=
355 input.position.y +
356 input.size.y;
357 }
358
360 {
361 auto inputsResult =
363 if (!inputsResult)
364 return;
365
366 auto &inputs = inputsResult.value().get();
367 for (const auto &e : inputs.entities()) {
368 inputs[e].isFocused = false;
369 inputs[e].showCursor = false;
370 inputs[e].blinkTimer = 0.0f;
371 }
372 }
373
374 void UISystem::focusTextInputAt(const sf::Vector2i &mousePos)
375 {
376 auto inputsResult =
378 if (!inputsResult)
379 return;
380
381 auto &inputs = inputsResult.value().get();
382
383 bool focusedOne = false;
384 for (const auto &e : inputs.entities()) {
385 auto &in = inputs[e];
386 if (!focusedOne && isMouseOverTextInput(in, mousePos)) {
387 in.isFocused = true;
388 in.showCursor = true;
389 in.blinkTimer = 0.0f;
390 focusedOne = true;
391 } else {
392 in.isFocused = false;
393 in.showCursor = false;
394 in.blinkTimer = 0.0f;
395 }
396 }
397 }
398
399 void UISystem::handleTextEntered(std::uint32_t unicode)
400 {
401 auto inputsResult =
403 if (!inputsResult)
404 return;
405
406 auto &inputs = inputsResult.value().get();
407
408 if (unicode == 8) {
409 for (const auto &e : inputs.entities()) {
410 auto &in = inputs[e];
411 if (!in.isFocused)
412 continue;
413 if (!in.value.empty()) {
414 in.value.pop_back();
415 if (in.onChange)
416 in.onChange(in.value);
417 }
418 }
419 return;
420 }
421 if (unicode == 13) {
422 for (const auto &e : inputs.entities()) {
423 auto &in = inputs[e];
424 if (!in.isFocused)
425 continue;
426 if (in.onSubmit)
427 in.onSubmit(in.value);
428 }
429 return;
430 }
431 if (unicode < 32) {
432 return;
433 }
434
435 if (unicode > 127) {
436 return;
437 }
438
439 const char c = static_cast<char>(unicode);
440
441 for (const auto &e : inputs.entities()) {
442 auto &in = inputs[e];
443 if (!in.isFocused)
444 continue;
445
446 if (in.value.size() >= in.maxLength)
447 return;
448
449 in.value.push_back(c);
450 if (in.onChange)
451 in.onChange(in.value);
452 return;
453 }
454 }
455
456 void UISystem::handleKeyPressed(sf::Keyboard::Key key)
457 {
458 auto inputsResult =
460 if (!inputsResult)
461 return;
462
463 auto &inputs = inputsResult.value().get();
464
465 if (key == sf::Keyboard::Key::Escape) {
467 return;
468 }
469
470 if (key == sf::Keyboard::Key::Backspace) {
471 for (const auto &e : inputs.entities()) {
472 auto &in = inputs[e];
473 if (!in.isFocused)
474 continue;
475
476 if (!in.value.empty()) {
477 in.value.pop_back();
478 if (in.onChange)
479 in.onChange(in.value);
480 }
481 return;
482 }
483 }
484
485 if (key == sf::Keyboard::Key::Enter) {
486 for (const auto &e : inputs.entities()) {
487 auto &in = inputs[e];
488 if (!in.isFocused)
489 continue;
490
491 if (in.onSubmit)
492 in.onSubmit(in.value);
493 return;
494 }
495 }
496 }
497
499 {
500 if (!_settings.getGamepadEnabled() || !sf::Joystick::isConnected(0))
501 return;
502
503 float deadzone = _settings.getGamepadDeadzone();
504 float speed = _settings.getGamepadCursorSpeed() * 200.0f * dt;
505
506 // Read left stick
507 float axisX = sf::Joystick::getAxisPosition(0, sf::Joystick::Axis::X);
508 float axisY = sf::Joystick::getAxisPosition(0, sf::Joystick::Axis::Y);
509
510 // If stick is being used, enable gamepad mode
511 if (std::abs(axisX) > deadzone || std::abs(axisY) > deadzone) {
512 _gamepadMode = true;
513
514 if (std::abs(axisX) > deadzone)
515 _gamepadCursorPos.x += (axisX / 100.0f) * speed;
516 if (std::abs(axisY) > deadzone)
517 _gamepadCursorPos.y += (axisY / 100.0f) * speed;
518 }
519
520 // D-pad alternative
521 float povX = sf::Joystick::getAxisPosition(0, sf::Joystick::Axis::PovX);
522 float povY = sf::Joystick::getAxisPosition(0, sf::Joystick::Axis::PovY);
523
524 if (std::abs(povX) > 50 || std::abs(povY) > 50) {
525 _gamepadMode = true;
526
527 if (povX < -50)
528 _gamepadCursorPos.x -= speed * 2.0f;
529 if (povX > 50)
530 _gamepadCursorPos.x += speed * 2.0f;
531 if (povY > 50)
532 _gamepadCursorPos.y -= speed * 2.0f;
533 if (povY < -50)
534 _gamepadCursorPos.y += speed * 2.0f;
535 }
536
537 // Clamp to window bounds
538 _gamepadCursorPos.x = std::clamp(_gamepadCursorPos.x, 0.0f, static_cast<float>(_window.getSize().x));
539 _gamepadCursorPos.y = std::clamp(_gamepadCursorPos.y, 0.0f, static_cast<float>(_window.getSize().y));
540
541 // If mouse moves, disable gamepad mode
542 static sf::Vector2i lastMousePos = sf::Mouse::getPosition(_window);
543 sf::Vector2i currentMousePos = sf::Mouse::getPosition(_window);
544 if (currentMousePos != lastMousePos) {
545 _gamepadMode = false;
546 }
547 lastMousePos = currentMousePos;
548 }
549
551 {
552 if (!_settings.getGamepadEnabled() || !sf::Joystick::isConnected(0) || !_gamepadMode)
553 return;
554
555 // Check validate button (A button by default)
556 static bool wasValidatePressed = false;
557 bool isValidatePressed = sf::Joystick::isButtonPressed(0, _settings.getGamepadValidateButton());
558
559 if (isValidatePressed && !wasValidatePressed) {
560 // Simulate a mouse click at gamepad cursor position
561 sf::Vector2i clickPos(static_cast<int>(_gamepadCursorPos.x), static_cast<int>(_gamepadCursorPos.y));
562 handleMouseClick(clickPos);
563 }
564
565 wasValidatePressed = isValidatePressed;
566 }
567
568 void UISystem::renderGamepadCursor(sf::RenderWindow& window)
569 {
571 return;
572
573 // Draw a circle cursor
574 sf::CircleShape cursor(12.0f);
575 cursor.setPosition({_gamepadCursorPos.x - 12.0f, _gamepadCursorPos.y - 12.0f});
576 cursor.setFillColor(sf::Color(0, 0, 0, 0));
577 cursor.setOutlineThickness(3.0f);
578 cursor.setOutlineColor(sf::Color(255, 200, 100));
579
580 // Inner dot
581 sf::CircleShape dot(4.0f);
582 dot.setPosition({_gamepadCursorPos.x - 4.0f, _gamepadCursorPos.y - 4.0f});
583 dot.setFillColor(sf::Color(255, 200, 100));
584
585 window.draw(cursor);
586 window.draw(dot);
587 }
588
590 {
591 auto entityRes = _registry.spawn();
592 if (!entityRes) {
593 log::error("Failed to spawn click sound event entity");
594 return;
595 }
596
597 ecs::Entity soundEntity = entityRes.value();
599 clickSound.soundPath = "assets/sounds/click.mp3";
600 clickSound.volume = 100.0f;
601 clickSound.pitch = 1.0f;
602
603 log::info("Creating click sound event with volume: {}", clickSound.volume);
604 _registry.add<ecs::components::audio::SoundEvent>(soundEntity, clickSound);
605 }
606
607} // namespace rtp::client
Logger declaration with support for multiple log levels.
bool getGamepadEnabled() const
Definition Settings.hpp:174
float getGamepadCursorSpeed() const
Definition Settings.hpp:224
unsigned int getGamepadValidateButton() const
Definition Settings.hpp:214
float getGamepadDeadzone() const
Definition Settings.hpp:184
void updateSliderValue(ecs::components::ui::Slider &slider, const sf::Vector2i &mousePos)
Definition UISystem.cpp:302
void handleMouseMove(const sf::Vector2i &mousePos)
Definition UISystem.cpp:62
sf::RenderWindow & _window
Definition UISystem.hpp:82
bool isMouseOverTextInput(const ecs::components::ui::TextInput &input, const sf::Vector2i &mousePos) const
Definition UISystem.cpp:343
ecs::Registry & _registry
Definition UISystem.hpp:81
void handleTextEntered(std::uint32_t unicode)
Definition UISystem.cpp:399
bool isMouseOverDropdown(const ecs::components::ui::Dropdown &dropdown, const sf::Vector2i &mousePos)
Definition UISystem.cpp:286
void focusTextInputAt(const sf::Vector2i &mousePos)
Definition UISystem.cpp:374
void handleEvent(const sf::Event &event)
Definition UISystem.cpp:52
void update(float dt) override
Update system logic for one frame.
Definition UISystem.cpp:17
void updateGamepadCursor(float dt)
Definition UISystem.cpp:498
sf::Vector2f _gamepadCursorPos
Definition UISystem.hpp:85
void handleKeyPressed(sf::Keyboard::Key key)
Definition UISystem.cpp:456
bool isMouseOverSlider(const ecs::components::ui::Slider &slider, const sf::Vector2i &mousePos)
Definition UISystem.cpp:270
bool isMouseOverButton(const ecs::components::ui::Button &button, const sf::Vector2i &mousePos)
Definition UISystem.cpp:254
void handleMouseClick(const sf::Vector2i &mousePos)
Definition UISystem.cpp:81
int getDropdownOptionAtMouse(const ecs::components::ui::Dropdown &dropdown, const sf::Vector2i &mousePos)
Definition UISystem.cpp:316
void renderGamepadCursor(sf::RenderWindow &window)
Definition UISystem.cpp:568
Represents an entity in the ECS (Entity-Component-System) architecture.
Definition Entity.hpp:63
auto spawn(void) -> std::expected< Entity, rtp::Error >
Definition Registry.cpp:51
auto add(Entity entity, Args &&...args) -> std::expected< std::reference_wrapper< T >, rtp::Error >
auto get(this const Self &self) -> std::expected< std::reference_wrapper< ConstLike< Self, SparseArray< T > > >, rtp::Error >
R-Type client namespace.
void error(LogFmt< std::type_identity_t< Args >... > fmt, Args &&...args) noexcept
Log an error message.
void info(LogFmt< std::type_identity_t< Args >... > fmt, Args &&...args) noexcept
Log an informational message.
Component for triggering one-time sound effects (SFX)
float volume
Volume level (0.0 - 1.0)
std::string soundPath
Path to the sound file.
Component representing a clickable button.
Definition Button.hpp:30
ButtonState state
Current state.
Definition Button.hpp:34
Vec2f position
Position of the button.
Definition Button.hpp:32
Vec2f size
Size of the button.
Definition Button.hpp:33
Component for dropdown menu selection.
Definition Dropdown.hpp:21
Vec2f position
Position of the dropdown.
Definition Dropdown.hpp:22
std::vector< std::string > options
Available options.
Definition Dropdown.hpp:24
Vec2f size
Size of the dropdown button.
Definition Dropdown.hpp:23
Component for a draggable slider control.
Definition Slider.hpp:19
Vec2f position
Position of the slider.
Definition Slider.hpp:20
float currentValue
Current value.
Definition Slider.hpp:24
Vec2f size
Size of the slider.
Definition Slider.hpp:21
float minValue
Minimum value.
Definition Slider.hpp:22
bool isDragging
Is currently being dragged.
Definition Slider.hpp:32
float maxValue
Maximum value.
Definition Slider.hpp:23
std::function< void(float)> onChange
Callback when value changes.
Definition Slider.hpp:25