/** * @file tools/virtualhid_control_model.cpp * @brief Testable model helper definitions for the native libvirtualhid control UI. */ // standard includes #include #include #include #include #include #include // local includes #include "virtualhid_control_model.hpp" namespace lvh::tools::virtualhid_control { namespace { void append_summary_separator(std::wostringstream &stream, bool wrote) { if (wrote) { stream << L" | "; } } } // namespace std::wstring device_type_name(DeviceType type) { switch (type) { using enum DeviceType; case gamepad: return L"gamepad"; case keyboard: return L"keyboard"; case mouse: return L"mouse"; case touchscreen: return L"touchscreen"; case trackpad: return L"trackpad"; case pen_tablet: return L"pen tablet"; } return L"unknown"; } std::wstring node_kind_name(DeviceNodeKind kind) { switch (kind) { using enum DeviceNodeKind; case input_event: return L"input"; case joystick: return L"joystick"; case hidraw: return L"hidraw"; case sysfs: return L"sysfs"; case other: return L"other"; } return L"other"; } std::wstring output_kind_name(GamepadOutputKind kind) { switch (kind) { using enum GamepadOutputKind; case rumble: return L"rumble"; case rgb_led: return L"rgb led"; case adaptive_triggers: return L"adaptive triggers"; case raw_report: return L"raw report"; case trigger_rumble: return L"trigger rumble"; case player_leds: return L"player leds"; } return L"raw report"; } std::wstring battery_state_name(GamepadBatteryState state) { switch (state) { using enum GamepadBatteryState; case unknown: return L"unknown"; case discharging: return L"discharging"; case charging: return L"charging"; case full: return L"full"; case voltage_or_temperature_error: return L"voltage/temperature error"; case temperature_error: return L"temperature error"; case charging_error: return L"charging error"; } return L"unknown"; } int battery_choice_index(GamepadBatteryState state) { for (std::size_t index = 0; index < battery_choices.size(); ++index) { if (battery_choices[index].state == state) { return static_cast(index); } } return 0; } std::wstring raw_hex(const std::vector &bytes) { std::wostringstream stream; stream << std::hex << std::setfill(L'0'); for (const auto value : bytes) { stream << std::setw(2) << static_cast(value); } return stream.str(); } std::optional profile_for_choice(const ProfileChoice &choice) { switch (choice.kind) { using enum GamepadProfileKind; case generic: return profiles::generic_gamepad(); case xbox_360: return profiles::xbox_360(); case xbox_one: return profiles::xbox_one(); case xbox_series: return profiles::xbox_series(); case dualshock4: return profiles::dualshock4(); case dualsense: return profiles::dualsense(); case switch_pro: return profiles::switch_pro(); } return std::nullopt; } MouseEvent mouse_event_for_action( MouseControlAction action, std::int32_t motion_step, std::int32_t scroll_step ) { using enum MouseControlAction; using enum MouseEventKind; switch (action) { case move_left: return MouseEvent {.kind = relative_motion, .x = -motion_step}; case move_right: return MouseEvent {.kind = relative_motion, .x = motion_step}; case move_up: return MouseEvent {.kind = relative_motion, .y = -motion_step}; case move_down: return MouseEvent {.kind = relative_motion, .y = motion_step}; case wheel_up: return MouseEvent { .kind = vertical_scroll, .high_resolution_scroll = scroll_step, }; case wheel_down: return MouseEvent { .kind = vertical_scroll, .high_resolution_scroll = -scroll_step, }; case pan_left: return MouseEvent { .kind = horizontal_scroll, .high_resolution_scroll = -scroll_step, }; case pan_right: return MouseEvent { .kind = horizontal_scroll, .high_resolution_scroll = scroll_step, }; } return {}; } MouseEvent mouse_button_event(MouseButton button, bool pressed) { return MouseEvent { .kind = MouseEventKind::button, .button = button, .pressed = pressed, }; } int axis_to_slider(float value) { return static_cast(std::lround(std::clamp(value, -1.0F, 1.0F) * static_cast(slider_scale))); } int trigger_to_slider(float value) { return static_cast(std::lround(std::clamp(value, 0.0F, 1.0F) * static_cast(slider_scale))); } float slider_to_float(long value) { return static_cast(value) / static_cast(slider_scale); } std::wstring yes_no(bool value) { return value ? L"yes" : L"no"; } std::string normalized_license_key(std::string_view license_key) { const auto is_whitespace = [](char character) { return std::isspace(static_cast(character)) != 0; }; const auto first = std::ranges::find_if_not(license_key, is_whitespace); const auto last = std::ranges::find_if_not(std::views::reverse(license_key), is_whitespace).base(); return first == license_key.end() ? std::string {} : std::string {first, last}; } bool supports_normalized_feedback(const DeviceProfile &profile) { using enum GamepadOutputKind; return supports_gamepad_output(profile, rumble) || supports_gamepad_output(profile, rgb_led) || supports_gamepad_output(profile, adaptive_triggers) || supports_gamepad_output(profile, trigger_rumble) || supports_gamepad_output(profile, player_leds); } std::wstring profile_feature_summary(const DeviceProfile &profile) { using enum GamepadOutputKind; const auto support = gamepad_profile_support(profile); std::wostringstream stream; stream << L"Features: battery " << yes_no(support.supports_battery); stream << L" | rumble " << yes_no(supports_gamepad_output(profile, rumble)); stream << L" | trigger rumble " << yes_no(supports_gamepad_output(profile, trigger_rumble)); stream << L" | RGB LED " << yes_no(supports_gamepad_output(profile, rgb_led)); stream << L" | player LEDs " << yes_no(supports_gamepad_output(profile, player_leds)); stream << L" | adaptive triggers " << yes_no(supports_gamepad_output(profile, adaptive_triggers)); stream << L" | raw output " << yes_no(supports_gamepad_output(profile, raw_report)); return stream.str(); } std::wstring device_feature_summary(const DeviceProfile &profile) { if (profile.device_type == DeviceType::mouse) { return L"Features: relative motion | five buttons | vertical wheel | horizontal wheel"; } return profile_feature_summary(profile); } bool append_latest_output_summary(std::wostringstream &stream, const OutputState &state) { auto wrote = false; if (state.latest_rumble) { stream << L"rumble low=" << state.latest_rumble->low_frequency_rumble << L" high=" << state.latest_rumble->high_frequency_rumble; wrote = true; } if (state.latest_trigger_rumble) { append_summary_separator(stream, wrote); stream << L"trigger rumble L=" << state.latest_trigger_rumble->left_trigger_rumble << L" R=" << state.latest_trigger_rumble->right_trigger_rumble; wrote = true; } if (state.latest_rgb_led) { append_summary_separator(stream, wrote); stream << L"RGB " << static_cast(state.latest_rgb_led->red) << L"," << static_cast(state.latest_rgb_led->green) << L"," << static_cast(state.latest_rgb_led->blue); wrote = true; } if (state.latest_adaptive_triggers) { append_summary_separator(stream, wrote); stream << L"adaptive flags=" << static_cast(state.latest_adaptive_triggers->adaptive_trigger_flags); wrote = true; } if (state.latest_player_leds) { append_summary_separator(stream, wrote); stream << L"player LEDs solid="; for (const auto active : state.latest_player_leds->player_leds) { stream << (active ? L'1' : L'0'); } stream << L" flashing="; for (const auto active : state.latest_player_leds->flashing_player_leds) { stream << (active ? L'1' : L'0'); } wrote = true; } if (!wrote && state.latest_raw_report) { stream << L"raw report"; wrote = true; } return wrote; } std::wstring output_summary(const OutputState &state, const DeviceProfile &profile) { std::wostringstream stream; stream << L"Output: "; if (state.outputs.empty()) { stream << L"no reports received"; } else if (!append_latest_output_summary(stream, state)) { stream << L"reports received"; } if (!supports_normalized_feedback(profile)) { stream << L" | profile has no normalized feedback categories"; } return stream.str(); } bool update_visible_controls_for_profile( const DeviceProfile &profile, std::array &visible_buttons, bool &battery_controls_visible ) { auto changed = false; for (std::size_t index = 0; index < button_choices.size(); ++index) { const auto visible = supports_gamepad_button(profile, button_choices[index].button); changed = changed || visible_buttons[index] != visible; visible_buttons[index] = visible; } changed = changed || battery_controls_visible != profile.capabilities.supports_battery; battery_controls_visible = profile.capabilities.supports_battery; return changed; } void record_output( OutputState &state, const GamepadOutput &output, std::uint64_t &next_sequence, std::size_t max_output_events ) { state.outputs.push_back({.sequence = next_sequence++, .output = output}); if (state.outputs.size() > max_output_events) { state.outputs.erase( state.outputs.begin(), state.outputs.begin() + static_cast(state.outputs.size() - max_output_events) ); } switch (output.kind) { using enum GamepadOutputKind; case rumble: state.latest_rumble = output; break; case trigger_rumble: state.latest_trigger_rumble = output; break; case rgb_led: state.latest_rgb_led = output; break; case adaptive_triggers: state.latest_adaptive_triggers = output; break; case player_leds: state.latest_player_leds = output; break; case raw_report: state.latest_raw_report = output; break; } } } // namespace lvh::tools::virtualhid_control