use std::rc::Rc;
use std::time::{Duration, Instant};
use crate::base::Point;
use crate::layout::{Dimension, Style as LayoutStyle};
use crate::reactive::{Scope, Signal};
use crate::render::Style;
use crate::theme::TokenSet;
use crate::ui::{dyn_view, Element, Phase, Role, UiEvent, View};
use super::SelectOption;
pub(crate) const TYPE_AHEAD_WINDOW: Duration = Duration::from_millis(900);
pub(crate) const DEFAULT_MAX_VISIBLE: usize = 8;
fn enabled_at(options: &[SelectOption], display: &[usize], pos: usize) -> bool {
display
.get(pos)
.and_then(|&i| options.get(i))
.is_some_and(|o| !o.disabled)
}
pub(crate) fn step_highlight(
options: &[SelectOption],
display: &[usize],
from: usize,
dir: i32,
) -> usize {
let n = display.len();
if n == 0 {
return 0;
}
let mut pos = from.min(n - 1) as i32;
loop {
pos += dir.signum();
if pos < 0 || pos >= n as i32 {
return from.min(n - 1);
}
if enabled_at(options, display, pos as usize) {
return pos as usize;
}
}
}
pub(crate) fn first_enabled(options: &[SelectOption], display: &[usize]) -> Option<usize> {
(0..display.len()).find(|&p| enabled_at(options, display, p))
}
pub(crate) fn last_enabled(options: &[SelectOption], display: &[usize]) -> Option<usize> {
(0..display.len())
.rev()
.find(|&p| enabled_at(options, display, p))
}
pub(crate) fn page_highlight(
options: &[SelectOption],
display: &[usize],
from: usize,
dir: i32,
page: usize,
) -> usize {
let n = display.len();
if n == 0 {
return 0;
}
let target = (from as i32 + dir.signum() * page as i32).clamp(0, n as i32 - 1) as usize;
if enabled_at(options, display, target) {
return target;
}
let onward = step_highlight(options, display, target, dir);
if onward != target && enabled_at(options, display, onward) {
return onward;
}
let back = step_highlight(options, display, target, -dir);
if enabled_at(options, display, back) {
return back;
}
from.min(n - 1)
}
#[derive(Default)]
pub(crate) struct TypeAhead {
buf: String,
at: Option<Instant>,
}
impl TypeAhead {
pub(crate) fn push(&mut self, ch: char, now: Instant) -> &str {
let stale = self
.at
.is_none_or(|t| now.duration_since(t) > TYPE_AHEAD_WINDOW);
if stale {
self.buf.clear();
}
self.at = Some(now);
self.buf.push(ch);
&self.buf
}
pub(crate) fn clear(&mut self) {
self.buf.clear();
self.at = None;
}
}
pub(crate) fn type_ahead_target(
options: &[SelectOption],
display: &[usize],
buf: &str,
current: usize,
) -> Option<usize> {
let lower = buf.to_lowercase();
let mut chars = lower.chars();
let first = chars.next()?;
let cycling = buf.chars().count() > 1 && chars.clone().all(|c| c == first);
let matches = |pos: usize, prefix: &str| {
enabled_at(options, display, pos)
&& options[display[pos]]
.label
.to_lowercase()
.starts_with(prefix)
};
let n = display.len();
if cycling {
let prefix = first.to_string();
return (1..=n)
.map(|d| (current + d) % n.max(1))
.find(|&p| matches(p, &prefix));
}
(0..n).find(|&p| matches(p, &lower))
}
pub(crate) struct TriggerLabel {
pub text: String,
pub short: Option<String>,
pub placeholder: bool,
}
pub(crate) fn trigger_view(
t: &TokenSet,
focused: Signal<bool>,
hovered: Signal<bool>,
disabled: bool,
label: Rc<dyn Fn() -> TriggerLabel>,
) -> View {
let text_fg = t.text;
let ground = t.surface;
let stroke = t.border;
let stroke_focus = t.border_focus;
let faint = t.text_faint;
let accent = t.accent;
dyn_view(
LayoutStyle::default()
.width(Dimension::Percent(1.0))
.height(Dimension::Cells(1)),
move || {
let TriggerLabel {
text: shown,
short,
placeholder: is_placeholder,
} = label();
let focus = focused.get();
let hover = hovered.get();
Element::new()
.style(
LayoutStyle::default()
.width(Dimension::Percent(1.0))
.height(Dimension::Cells(1)),
)
.draw(move |canvas, rect| {
if rect.is_empty() || rect.w < 4 {
return;
}
let ink = if disabled || is_placeholder {
faint
} else {
text_fg
};
let style = Style::new().fg(ink).bg(ground);
canvas.fill_styled(rect, ' ', &style);
let stroke_style = Style::new()
.fg(if focus && !disabled {
stroke_focus
} else {
stroke
})
.bg(ground);
canvas.print_styled(rect.origin(), "▐", &stroke_style);
canvas.print_styled(Point::new(rect.right() - 1, rect.y), "▌", &stroke_style);
let room = (rect.w - 4).max(0);
let shown = match &short {
Some(s) if crate::text::width(&shown) > room => s.as_str(),
_ => shown.as_str(),
};
let mut col = 0;
for seg in crate::text::segments(shown) {
if col + seg.width > room {
break;
}
canvas.print_styled(
Point::new(rect.x + 1 + col, rect.y),
seg.cluster,
&style,
);
col += seg.width;
}
let chevron_ink = if disabled {
faint
} else if hover || focus {
accent
} else {
text_fg
};
canvas.print_styled(
Point::new(rect.right() - 2, rect.y),
"▾",
&Style::new().fg(chevron_ink).bg(ground),
);
})
.build()
},
)
}
pub(crate) struct OptionRows {
pub options: Rc<Vec<SelectOption>>,
pub display: Signal<Vec<usize>>,
pub highlight: Signal<usize>,
pub checks: Option<Signal<Vec<String>>>,
pub max_visible: usize,
pub on_activate: Rc<dyn Fn(usize)>,
}
pub(crate) fn option_rows_view(t: &TokenSet, rows: OptionRows) -> View {
let ink = t.text;
let ground = t.surface_raised;
let muted = t.text_muted;
let faint = t.text_faint;
let sel_fg = t.selection_fg;
let sel_bg = t.selection_bg;
let OptionRows {
options,
display,
highlight,
checks,
max_visible,
on_activate,
} = rows;
dyn_view(
LayoutStyle::column()
.width(Dimension::Percent(1.0))
.grow(1.0)
.shrink(1.0),
move || {
let disp = display.get();
let n = disp.len();
let h = highlight.get().min(n.saturating_sub(1));
let checked: Option<Vec<String>> = checks.map(|c| c.get());
let vis = max_visible.min(n.max(1));
let start = (if h < vis { 0 } else { h + 1 - vis }).min(n.saturating_sub(vis));
let mut col = Element::new().style(
LayoutStyle::column()
.width(Dimension::Percent(1.0))
.shrink(0.0),
);
for (pos, &opt_ix) in disp.iter().enumerate().skip(start).take(vis) {
let Some(opt) = options.get(opt_ix) else {
continue;
};
let is_highlight = pos == h;
let disabled = opt.disabled;
let label = opt.label.clone();
let hint = opt.hint.clone();
let mark = checked.as_ref().map(|set| set.contains(&opt.key));
let on_activate = on_activate.clone();
col = col.child(
Element::new()
.style(LayoutStyle::line(1).shrink(0.0))
.role(Role::MenuItem)
.access_label(label.clone())
.on(Phase::Bubble, move |ctx, ev| {
if let UiEvent::Mouse(m) = ev {
if matches!(
m.kind,
crate::ui::MouseKind::Down(crate::ui::MouseButton::Left)
) {
if !disabled {
on_activate(pos);
}
ctx.stop_propagation();
}
}
})
.draw(move |canvas, rect| {
let (fg, bg) = if disabled {
(faint, ground)
} else if is_highlight {
(sel_fg, sel_bg)
} else {
(ink, ground)
};
let style = Style::new().fg(fg).bg(bg);
canvas.fill_styled(rect, ' ', &style);
let mut x = rect.x + 1;
if let Some(on) = mark {
canvas.print_styled(
Point::new(x, rect.y),
if on { "[x]" } else { "[ ]" },
&style,
);
x += 4;
}
canvas.print_styled(Point::new(x, rect.y), &label, &style);
if let Some(hint) = &hint {
let hw = crate::text::width(hint);
let hx = rect.right() - 1 - hw;
if hx > x + crate::text::width(&label) + 1 {
let hint_ink = if disabled {
faint
} else if is_highlight {
sel_fg
} else {
muted
};
canvas.print_styled(
Point::new(hx, rect.y),
hint,
&Style::new().fg(hint_ink).bg(bg),
);
}
}
})
.build(),
);
}
col.build()
},
)
}
pub(crate) fn resolve_overlays(
cx: Scope,
explicit: Option<super::super::overlays::Overlays>,
) -> Option<super::super::overlays::Overlays> {
explicit.or_else(|| cx.use_context::<super::super::overlays::Overlays>())
}