use iced::{Point, Rectangle, Size};
use scrive_core::PopupList;
pub const POPUP_MAX_VISIBLE: usize = 12;
pub const POPUP_MIN_CH: usize = 20;
pub const POPUP_MAX_CH: usize = 60;
pub const POPUP_PAD: f32 = 4.0;
pub const POPUP_DOC_MAX_LINES: usize = 4;
#[must_use]
pub fn window_start(selected: usize, filtered: usize) -> usize {
if filtered <= POPUP_MAX_VISIBLE {
0
} else {
selected.saturating_sub(POPUP_MAX_VISIBLE - 1).min(filtered - POPUP_MAX_VISIBLE)
}
}
#[must_use]
pub fn row_y(origin_y: f32, vis: usize, line_h: f32) -> f32 {
origin_y + POPUP_PAD + vis as f32 * line_h
}
#[must_use]
pub fn row_at(origin_y: f32, y: f32, line_h: f32, visible: usize) -> Option<usize> {
let row = ((y - origin_y - POPUP_PAD) / line_h).floor();
(row >= 0.0 && (row as usize) < visible).then_some(row as usize)
}
#[must_use]
pub fn selected_doc(list: &PopupList) -> Option<&str> {
list.filtered
.get(list.selected as usize)
.and_then(|&i| list.items[i as usize].doc.as_deref())
}
#[must_use]
pub fn width_cells(list: &PopupList) -> usize {
let n = list.filtered.len();
let visible = n.min(POPUP_MAX_VISIBLE);
let start = window_start(list.selected as usize, n);
let widest = list.filtered[start..start + visible]
.iter()
.map(|&i| {
let it = &list.items[i as usize];
it.label.chars().count() + it.detail.as_ref().map_or(0, |d| d.chars().count() + 2)
})
.max()
.unwrap_or(0);
widest.clamp(POPUP_MIN_CH, POPUP_MAX_CH)
}
#[must_use]
pub fn doc_lines(list: &PopupList, width_cells: usize) -> usize {
selected_doc(list).map_or(0, |d| d.chars().count().div_ceil(width_cells.max(1)).clamp(1, POPUP_DOC_MAX_LINES))
}
#[must_use]
pub fn extent(list: &PopupList, advance: f32, line_h: f32) -> Size {
let cells = width_cells(list);
let visible = list.filtered.len().min(POPUP_MAX_VISIBLE);
let rows = visible + doc_lines(list, cells);
Size::new(cells as f32 * advance + 2.0 * POPUP_PAD, rows as f32 * line_h + 2.0 * POPUP_PAD)
}
#[must_use]
pub fn place(anchor_x: f32, caret_top: f32, caret_bottom: f32, size: Size, bounds: Rectangle) -> Point {
let x = anchor_x.clamp(bounds.x, (bounds.x + bounds.width - size.width).max(bounds.x));
let y = if caret_bottom + size.height <= bounds.y + bounds.height {
caret_bottom
} else {
(caret_top - size.height).max(bounds.y)
};
Point::new(x, y)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn window_keeps_selected_visible() {
assert_eq!(window_start(0, 5), 0);
assert_eq!(window_start(4, 5), 0);
assert_eq!(window_start(0, 30), 0);
assert_eq!(window_start(11, 30), 0); assert_eq!(window_start(12, 30), 1); assert_eq!(window_start(29, 30), 18); }
#[test]
fn place_below_then_flips_above_when_tight() {
let bounds = Rectangle { x: 0.0, y: 0.0, width: 400.0, height: 300.0 };
let size = Size::new(120.0, 80.0);
assert_eq!(place(50.0, 20.0, 40.0, size, bounds).y, 40.0);
assert_eq!(place(50.0, 280.0, 300.0, size, bounds).y, 200.0);
}
#[test]
fn place_clamps_x_into_bounds() {
let bounds = Rectangle { x: 0.0, y: 0.0, width: 400.0, height: 300.0 };
let size = Size::new(120.0, 80.0);
assert_eq!(place(350.0, 20.0, 40.0, size, bounds).x, 280.0); assert_eq!(place(-10.0, 20.0, 40.0, size, bounds).x, 0.0);
}
}