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
This commit is contained in:
fanlusky committed 2026-07-29 12:18:13 +08:00
1 parent 06173c1e25
commit e41bee9a20
4 files changed
+79 -6

No files matched your search

+12 -3
View File
@@ -1,4 +1,5 @@
#pragma once
#include <algorithm>
#include <string>
#include <string_view>
#include <unordered_set>
@@ -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;
}
+17 -3
View File
@@ -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;
+1
View File
@@ -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"
@@ -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);
}