From e41bee9a201203faae359d4458d3ee8d199c03a6 Mon Sep 17 00:00:00 2001 From: fanlusky <1730976608@qq.com> Date: Wed, 29 Jul 2026 12:18:13 +0800 Subject: [PATCH] feat(candidate): support cross-page arrow navigation - move down from the last candidate to the next page - move up from the first candidate to the previous page - preserve selection when expanding initial candidates - keep navigation bounded at the first and last candidates - add regression tests for cross-page selection --- server/src/global/globals.h | 15 ++++-- server/src/ipc/event_listener.cpp | 20 ++++++-- server/tests/CMakeLists.txt | 1 + server/tests/src/test_candidate_ui_state.cpp | 49 ++++++++++++++++++++ 4 files changed, 79 insertions(+), 6 deletions(-) create mode 100644 server/tests/src/test_candidate_ui_state.cpp diff --git a/server/src/global/globals.h b/server/src/global/globals.h index 17e03196..e06eb710 100644 --- a/server/src/global/globals.h +++ b/server/src/global/globals.h @@ -1,4 +1,5 @@ #pragma once +#include #include #include #include @@ -132,18 +133,26 @@ struct CandidateUiState bool move_selection(int offset) { + if (page_size <= 0) + { + page_index = 0; + selected_index_in_page = 0; + return false; + } const int count = current_page_count(); if (count <= 0) { selected_index_in_page = 0; return false; } - const int next = selected_index_in_page + offset; - if (next < 0 || next >= count) + selected_index_in_page = std::clamp(selected_index_in_page, 0, count - 1); + const int next = current_page_start() + selected_index_in_page + offset; + if (next < 0 || next >= item_total_count) { return false; } - selected_index_in_page = next; + page_index = next / page_size; + selected_index_in_page = next % page_size; return true; } diff --git a/server/src/ipc/event_listener.cpp b/server/src/ipc/event_listener.cpp index 7aea61f6..0763440d 100644 --- a/server/src/ipc/event_listener.cpp +++ b/server/src/ipc/event_listener.cpp @@ -2391,11 +2391,11 @@ void HandleImeKey(uint64_t client_id, uint64_t activation_epoch, uint64_t reques UINT result = Global::DataFromServerMsgType::NavigationIgnored; bool refresh = false; - const auto move_page = [&](int offset, UINT response_type) { - result = response_type; - if (offset > 0 && ui.page_index == 0 && g_inputSession->expand_initial_candidates()) + const auto expand_initial_candidates = [&] { + if (g_inputSession->expand_initial_candidates()) { const int current_page = ui.page_index; + const int current_selection = ui.selected_index_in_page; auto expanded = g_inputSession->get_candidates(); user_dictionary::apply_fixed_positions( user_dictionary::default_user_db_path(), CurrentRankingContextKey(), expanded, true, @@ -2404,6 +2404,16 @@ void HandleImeKey(uint64_t client_id, uint64_t activation_epoch, uint64_t reques }); ui.set_items(std::move(expanded)); ui.page_index = current_page; + ui.selected_index_in_page = current_selection; + return true; + } + return false; + }; + const auto move_page = [&](int offset, UINT response_type) { + result = response_type; + if (offset > 0 && ui.page_index == 0) + { + expand_initial_candidates(); } if (offset < 0 ? ui.has_prev_page() : ui.has_next_page()) { @@ -2413,6 +2423,10 @@ void HandleImeKey(uint64_t client_id, uint64_t activation_epoch, uint64_t reques }; const auto move_selection = [&](int offset, UINT response_type) { result = response_type; + if (offset > 0 && ui.selected_index_in_page + 1 >= ui.current_page_count()) + { + expand_initial_candidates(); + } if (ui.move_selection(offset)) { refresh = true; diff --git a/server/tests/CMakeLists.txt b/server/tests/CMakeLists.txt index 17e03414..3efef2c9 100644 --- a/server/tests/CMakeLists.txt +++ b/server/tests/CMakeLists.txt @@ -14,6 +14,7 @@ set(TEST_SOURCE_FILES "${CMAKE_CURRENT_SOURCE_DIR}/src/test_input_key_policy.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/test_candidate_ui_owner.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/test_candidate_ui_action_policy.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/test_candidate_ui_state.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/test_ipc_protocol_constants.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/test_floating_toolbar_visibility_policy.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/test_terminal_deactivation_policy.cpp" diff --git a/server/tests/src/test_candidate_ui_state.cpp b/server/tests/src/test_candidate_ui_state.cpp new file mode 100644 index 00000000..2694b987 --- /dev/null +++ b/server/tests/src/test_candidate_ui_state.cpp @@ -0,0 +1,49 @@ +#include "global/globals.h" +#include "tests/includes/test_framework.h" + +namespace +{ +Global::CandidateUiState MakeCandidateUiState(int item_count, int page_size) +{ + Global::CandidateUiState state; + state.item_total_count = item_count; + state.page_size = page_size; + return state; +} +} + +TEST_CASE(candidate_ui_selection_moves_from_page_end_to_next_page_start) +{ + auto state = MakeCandidateUiState(8, 3); + state.page_index = 0; + state.selected_index_in_page = 2; + + REQUIRE(state.move_selection(1)); + REQUIRE_EQ(state.page_index, 1); + REQUIRE_EQ(state.selected_index_in_page, 0); +} + +TEST_CASE(candidate_ui_selection_moves_from_page_start_to_previous_page_end) +{ + auto state = MakeCandidateUiState(8, 3); + state.page_index = 1; + state.selected_index_in_page = 0; + + REQUIRE(state.move_selection(-1)); + REQUIRE_EQ(state.page_index, 0); + REQUIRE_EQ(state.selected_index_in_page, 2); +} + +TEST_CASE(candidate_ui_selection_stops_at_first_and_last_candidate) +{ + auto state = MakeCandidateUiState(8, 3); + REQUIRE(!state.move_selection(-1)); + REQUIRE_EQ(state.page_index, 0); + REQUIRE_EQ(state.selected_index_in_page, 0); + + state.page_index = 2; + state.selected_index_in_page = 1; + REQUIRE(!state.move_selection(1)); + REQUIRE_EQ(state.page_index, 2); + REQUIRE_EQ(state.selected_index_in_page, 1); +}