use crate::Position;
pub struct RadialInput {
pub history_len: usize, pub candidate_count: usize,
}
pub struct RadialPositions {
pub focus: Position,
pub history: Vec<Position>, pub candidates: Vec<Position>,}
pub struct RadialLayout {
pub hist_x: f32, pub hist_gap: f32, pub cand_radius: f32, pub cand_spread: f32,}
impl Default for RadialLayout {
fn default() -> Self {
Self { hist_x: -3.0, hist_gap: 1.4, cand_radius: 3.0, cand_spread: 0.9 }
}
}
impl RadialLayout {
pub fn compute(&self, input: &RadialInput) -> RadialPositions {
let focus = [0.0, 0.0];
let n = input.history_len;
let history: Vec<Position> = (0..n)
.map(|i| {
let recency = (n - 1 - i) as f32;
[self.hist_x, recency * self.hist_gap]
})
.collect();
let m = input.candidate_count;
let candidates: Vec<Position> = (0..m)
.map(|j| {
let t = if m <= 1 { 0.5 } else { j as f32 / (m as f32 - 1.0) }; let angle = (t - 0.5) * std::f32::consts::PI * self.cand_spread; [self.cand_radius * angle.cos(), self.cand_radius * angle.sin()]
})
.collect();
RadialPositions { focus, history, candidates }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn focus_at_origin() {
let out = RadialLayout::default().compute(&RadialInput { history_len: 0, candidate_count: 0 });
assert_eq!(out.focus, [0.0, 0.0]);
assert!(out.history.is_empty());
assert!(out.candidates.is_empty());
}
#[test]
fn history_is_a_left_column_newest_nearest() {
let out = RadialLayout::default().compute(&RadialInput { history_len: 3, candidate_count: 0 });
assert_eq!(out.history.len(), 3);
assert!(out.history.iter().all(|p| p[0] < 0.0));
let oldest_y = out.history[0][1].abs();
let newest_y = out.history[2][1].abs();
assert!(newest_y < oldest_y, "newest should sit nearer the focus than oldest");
}
#[test]
fn candidates_fan_to_the_right() {
let out = RadialLayout::default().compute(&RadialInput { history_len: 0, candidate_count: 4 });
assert_eq!(out.candidates.len(), 4);
assert!(out.candidates.iter().all(|p| p[0] > 0.0), "candidates must be on the right");
}
}