1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
//! Horizontal scroll window calculation utilities
//!
//! Calculate the visible window of items that fit within available width,
//! ensuring the selected item is always visible. Uses edge-based scrolling.
use serde::{Deserialize, Serialize};
/// Horizontal scroll window result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HorizontalScrollWindow {
/// Start index of visible items
pub start_index: usize,
/// End index of visible items (exclusive)
pub end_index: usize,
/// Whether to show left arrow
pub show_left_arrow: bool,
/// Whether to show right arrow
pub show_right_arrow: bool,
}
/// Calculate the visible window of items that fit within available width,
/// ensuring the selected item is always visible. Uses edge-based scrolling:
/// the window only scrolls when the selected item would be outside the visible
/// range, and positions the selected item at the edge (not centered).
///
/// # Arguments
/// * `item_widths` - Array of item widths (each width should include separator if applicable)
/// * `available_width` - Total available width for items
/// * `arrow_width` - Width of scroll indicator arrow (including space)
/// * `selected_idx` - Index of selected item (must stay visible)
/// * `first_item_has_separator` - Whether first item's width includes a separator that should be ignored
///
/// # Returns
/// Visible window bounds and whether to show scroll arrows
pub fn calculate_horizontal_scroll_window(
item_widths: &[usize],
available_width: usize,
arrow_width: usize,
selected_idx: usize,
first_item_has_separator: bool,
) -> HorizontalScrollWindow {
let total_items = item_widths.len();
if total_items == 0 {
return HorizontalScrollWindow {
start_index: 0,
end_index: 0,
show_left_arrow: false,
show_right_arrow: false,
};
}
// Clamp selected_idx to valid range
let clamped_selected = selected_idx.min(total_items - 1);
// If all items fit, show them all
let total_width: usize = item_widths.iter().sum();
if total_width <= available_width {
return HorizontalScrollWindow {
start_index: 0,
end_index: total_items,
show_left_arrow: false,
show_right_arrow: false,
};
}
// Calculate cumulative widths for efficient range calculations
let mut cumulative_widths: Vec<usize> = vec![0; total_items + 1];
for i in 0..total_items {
cumulative_widths[i + 1] = cumulative_widths[i] + item_widths[i];
}
// Helper to get width of range [start, end)
let range_width = |start: usize, end: usize| -> usize {
let base_width = cumulative_widths[end] - cumulative_widths[start];
// When starting after index 0 and first item has separator baked in,
// subtract 1 because we don't render leading separator on first visible item
if first_item_has_separator && start > 0 {
base_width.saturating_sub(1)
} else {
base_width
}
};
// Calculate effective available width based on whether we'll show arrows
let get_effective_width = |start: usize, end: usize| -> usize {
let mut width = available_width;
if start > 0 {
width -= arrow_width; // left arrow
}
if end < total_items {
width -= arrow_width; // right arrow
}
width
};
// Edge-based scrolling: Start from the beginning and only scroll when necessary
let mut start_index = 0;
let mut end_index = 1;
// Expand from start as much as possible
while end_index < total_items
&& range_width(start_index, end_index + 1)
<= get_effective_width(start_index, end_index + 1)
{
end_index += 1;
}
// If selected is within visible range, we're done
if clamped_selected >= start_index && clamped_selected < end_index {
return HorizontalScrollWindow {
start_index,
end_index,
show_left_arrow: start_index > 0,
show_right_arrow: end_index < total_items,
};
}
// Selected is outside visible range - need to scroll
if clamped_selected >= end_index {
// Selected is to the right - scroll so selected is at the right edge
end_index = clamped_selected + 1;
start_index = clamped_selected;
// Expand left as much as possible (selected stays at right edge)
while start_index > 0
&& range_width(start_index - 1, end_index)
<= get_effective_width(start_index - 1, end_index)
{
start_index -= 1;
}
} else {
// Selected is to the left - scroll so selected is at the left edge
start_index = clamped_selected;
end_index = clamped_selected + 1;
// Expand right as much as possible (selected stays at left edge)
while end_index < total_items
&& range_width(start_index, end_index + 1)
<= get_effective_width(start_index, end_index + 1)
{
end_index += 1;
}
}
HorizontalScrollWindow {
start_index,
end_index,
show_left_arrow: start_index > 0,
show_right_arrow: end_index < total_items,
}
}