# How Hallelujah IM Handles Candidate Window Pagination and Scrolling

> Learn how Hallelujah IM handles candidate window pagination and scrolling by managing the candidate list and leveraging IMKCandidates methods for efficient navigation.

- Repository: [dongyuwei/hallelujahim](https://github.com/dongyuwei/hallelujahim)
- Tags: internals
- Published: 2026-02-28

---

**Hallelujah IM implements candidate window pagination by storing the full candidate list in `_candidates` and calculating page offsets with a fixed page size of 9, while delegating visual scrolling to `IMKCandidates`’ `moveUp:` and `moveDown:` methods.**

The Hallelujah IM input method, maintained in the `dongyuwei/hallelujahim` repository, converts Latin character sequences into Chinese candidates using a custom **InputController** paired with Apple’s Input Method Kit. Its approach to candidate window pagination and scrolling combines manual index tracking with macOS-provided UI components to manage lists that exceed the visible page capacity.

## Pagination Logic and Page Size Configuration

### Fixed Page Size of 9 Candidates

The controller enforces a strict page size limit of **9 entries** per visible page. When the candidate list exceeds this threshold, the full set remains stored in the mutable array `_candidates`, while only a subset renders in the floating window. The currently highlighted position across the entire list—regardless of which page is visible—is tracked by the integer `_currentCandidateIndex`, which uses **1-based indexing**.

In `src/InputController.mm` at lines 155–162, the logic handles digit-key selection (1–9) by computing the correct array offset based on the current page:

```objc
int pageSize = 9;
if (_currentCandidateIndex <= pageSize) {
    candidate = _candidates[pressedNumber - 1];
} else {
    // page‑size‑based offset calculation
    candidate = _candidates[pageSize * (_currentCandidateIndex / pageSize - 1)
                         + (_currentCandidateIndex % pageSize)
                         + pressedNumber - 1];
}

```

This arithmetic determines which element to pull from `_candidates` when the user has scrolled beyond the first page. After selection, the controller resets the composition buffers and commits the chosen word.

### Array Index Calculation

The pagination formula uses integer division and modulo operations to map the visible page number and the pressed digit to an absolute index in the master candidate array. The term `(_currentCandidateIndex / pageSize - 1)` identifies the current page offset, while `(_currentCandidateIndex % pageSize)` accounts for the remainder within the active page.

## Arrow Key Scrolling Implementation

### Delegating to IMKCandidates

Visual scrolling is handled by the **IMKCandidates** instance named `sharedCandidates`, which the controller instantiates during initialization. When the user presses the **↑** or **↓** arrow keys while the candidate window is visible, the controller delegates the UI update to the framework and manually synchronizes the logical index.

Lines 33–44 of `src/InputController.mm` implement this behavior:

```objc
if (keyCode == KEY_ARROW_DOWN) {
    [sharedCandidates moveDown:self];
    _currentCandidateIndex++;
    return NO;
}
if (keyCode == KEY_ARROW_UP)   {
    [sharedCandidates moveUp:self];
    _currentCandidateIndex--;
    return NO;
}

```

The `moveDown:` and `moveUp:` methods automatically scroll the candidate view when the highlight moves past the visible boundary. The controller increments or decrements `_currentCandidateIndex` to maintain consistency between the UI state and the internal selection tracker.

## Candidate List Synchronization

### Updating the UI Buffer

Whenever the user inserts or deletes characters, the controller refreshes the candidate window by calling `updateCandidates` followed by `show:` on the shared instance. This occurs in `src/InputController.mm` at lines 125–127 and again in the `deleteBackward:` method around lines 200–204:

```objc
[sharedCandidates updateCandidates];
[sharedCandidates show:kIMKLocateCandidatesBelowHint];

```

The `updateCandidates` message triggers the controller’s `candidates:` delegate method, which fetches fresh results from the **ConversionEngine**:

```objc
- (NSArray *)candidates:(id)sender {
    NSString *originalInput = [self originalBuffer];
    NSArray *candidateList = [engine getCandidates:originalInput];
    _candidates = [NSMutableArray arrayWithArray:candidateList];
    return candidateList;
}

```

This method, located at lines 12–16 of `src/InputController.mm`, repopulates `_candidates` and returns the array to the Input Method Kit for rendering.

## User Interaction Flow

The complete interaction flow for pagination and scrolling follows these steps:

1. **Input Detection:** `onKeyEvent:` appends typed Latin characters to the original buffer and invokes `updateCandidates` and `show` to display the window.
2. **Initial Render:** The candidate window appears with up to 9 items from page 0.
3. **Scrolling:** Pressing ↑ or ↓ calls `moveUp:` or `moveDown:` on `sharedCandidates`, which scrolls the view while the controller adjusts `_currentCandidateIndex` across the entire candidate set.
4. **Pagination Selection:** Pressing a digit (1–9) executes the page-math formula to select the correct entry from `_candidates` based on the current page derived from `_currentCandidateIndex`.
5. **Commit:** The controller replaces the composition buffer with the selected candidate, commits the composition, and hides the candidate UI.

## Summary

- **Hallelujah IM** stores all candidates in `_candidates` but displays only 9 per page.
- **Pagination math** in `src/InputController.mm` (lines 155–162) uses integer division and modulo to map digit keys to the correct array index across multiple pages.
- **Arrow-key scrolling** delegates to `IMKCandidates`’ `moveUp:` and `moveDown:` methods at lines 33–44, while `_currentCandidateIndex` tracks the logical position.
- **UI synchronization** occurs through `updateCandidates` and the `candidates:` data source method, which queries `ConversionEngine` for fresh results.
- The **1-based index** `_currentCandidateIndex` spans the entire candidate list, not just the visible page, enabling consistent selection across pagination boundaries.

## Frequently Asked Questions

### How does Hallelujah IM calculate which candidate is selected when a user presses a number key?

The input method uses the 1-based `_currentCandidateIndex` to determine the current page. If the index exceeds the page size of 9, it calculates the absolute array position using the formula `pageSize * (_currentCandidateIndex / pageSize - 1) + (_currentCandidateIndex % pageSize) + pressedNumber - 1` to retrieve the correct entry from `_candidates`.

### Which macOS framework renders the candidate window and handles its visual scrolling?

Hallelujah IM uses the **Input Method Kit (IMKit)** framework. Specifically, it instantiates `IMKCandidates` as `sharedCandidates` and calls `moveUp:` and `moveDown:` to handle visual scrolling, while the framework manages the actual window rendering and paging animations.

### How many candidates does Hallelujah IM display per page?

The implementation enforces a fixed **page size of 9 candidates**. This constant is hardcoded in `src/InputController.mm` and determines both the UI layout and the mathematical pagination logic used for number-key selection.

### What happens to the internal state after a user selects a candidate from a paginated list?

Upon selection, the controller calls `cancelComposition`, updates the composed and original buffers with the selected string, invokes `commitComposition:` to send the text to the host application, and resets the candidate index. This sequence clears the candidate window and prepares the input method for the next composition session.