use crate::base::{Point, Rgba, Size};
use crate::layout::{Dimension, Style as LayoutStyle};
use crate::render::Style;
use crate::ui::{Element, EventCtx, MouseButton, MouseKind, Phase, Role, UiEvent, View};
use super::ChoiceQuestion;
pub(crate) const GLYPH_W: i32 = 2;
pub(crate) const MIN_INNER_W: i32 = 24;
pub(crate) struct Geometry {
pub panel: Size,
pub prompt_lines: Vec<String>,
pub region_rows: i32,
pub body_rows: i32,
}
pub(crate) fn option_height(o: &super::ChoiceOption) -> i32 {
if o.detail.is_some() {
2
} else {
1
}
}
pub(crate) fn keys_summary(q: &ChoiceQuestion) -> String {
let keys: Vec<String> = q
.options
.iter()
.filter_map(|o| o.key.map(|k| k.to_string()))
.collect();
keys.join("/")
}
pub(crate) fn has_buttons(multiple: bool, dismissable: bool) -> bool {
multiple || dismissable
}
pub(crate) fn dismiss_button_label(custom: Option<&str>) -> &str {
custom.unwrap_or("Cancel")
}
pub(crate) fn esc_segment(dismiss_label: Option<&str>) -> String {
match dismiss_label {
Some(label) => format!("Esc {label}"),
None => String::from("Esc cancels"),
}
}
pub(crate) fn measure(
q: &ChoiceQuestion,
viewport: Size,
max_visible: i32,
dismissable: bool,
dismiss_label: Option<&str>,
body_rows_pref: Option<i32>,
body_width_pref: Option<i32>,
) -> Geometry {
let opt_w = q
.options
.iter()
.map(|o| {
let key_w = if o.key.is_some() { 4 } else { 0 };
let label = GLYPH_W + crate::text::width(&o.label) + key_w;
let detail = o
.detail
.as_ref()
.map(|d| GLYPH_W + crate::text::width(d))
.unwrap_or(0);
label.max(detail)
})
.max()
.unwrap_or(0);
let other_w = q
.other
.as_ref()
.map(|l| GLYPH_W + crate::text::width(l))
.unwrap_or(0);
let prompt_w = crate::text::width(&q.prompt).min(52);
let confirm_w = if q.allow_multiple {
crate::text::width("Confirm") + 2
} else {
0
};
let dismiss_w = if dismissable {
crate::text::width(dismiss_button_label(dismiss_label)) + 2
} else {
0
};
let buttons_w = confirm_w + dismiss_w + if confirm_w > 0 && dismiss_w > 0 { 2 } else { 0 };
let esc = dismissable.then(|| esc_segment(dismiss_label));
let hint_w = crate::text::width(
&hint_segments(q.allow_multiple, esc.as_deref(), &keys_summary(q)).join(" · "),
);
let body_w = body_width_pref.unwrap_or(0);
let natural = opt_w
.max(other_w)
.max(prompt_w)
.max(buttons_w)
.max(hint_w)
.max(body_w)
.max(MIN_INNER_W);
let inner_w = natural.min((viewport.w - 6).max(12));
let prompt_cap = ((viewport.h / 3).max(2)) as usize;
let mut prompt_lines = crate::text::wrap(&q.prompt, inner_w);
if prompt_lines.len() > prompt_cap {
prompt_lines.truncate(prompt_cap);
if let Some(last) = prompt_lines.last_mut() {
*last = crate::text::truncate_ellipsis(&format!("{last}…"), inner_w);
}
}
let region_full: i32 = q.options.iter().map(option_height).sum();
let fixed = prompt_lines.len() as i32
+ 1
+ i32::from(body_rows_pref.is_some())
+ if q.other.is_some() { 2 } else { 0 }
+ 1
+ i32::from(has_buttons(q.allow_multiple, dismissable))
+ 1;
let avail = (viewport.h - 2 - fixed).max(1);
let region_rows = region_full
.min(max_visible)
.min(avail)
.max(i32::from(!q.options.is_empty()));
let body_rows = match body_rows_pref {
Some(pref) => (avail - region_rows).min(pref.max(1)).max(1),
None => 0,
};
let panel = Size::new(
(inner_w + 2).min(viewport.w),
(fixed + region_rows + body_rows + 2).min(viewport.h),
);
Geometry {
panel,
prompt_lines,
region_rows,
body_rows,
}
}
pub(crate) fn clamp_row(row: usize, total: usize) -> usize {
row.min(total.saturating_sub(1))
}
pub(crate) fn hint_segments(multiple: bool, esc: Option<&str>, keys: &str) -> Vec<String> {
let mut segs = Vec::new();
if !keys.is_empty() {
segs.push(format!("{keys} pick"));
}
if multiple {
segs.push(String::from("Space toggles"));
}
segs.push(String::from("Enter confirms"));
if let Some(esc) = esc {
segs.push(esc.to_string());
}
segs
}
pub(crate) fn window_start(heights: &[i32], budget: i32, anchor: usize) -> usize {
if heights.is_empty() {
return 0;
}
let anchor = anchor.min(heights.len() - 1);
let mut start = anchor;
let mut used = heights[anchor];
while start > 0 && used + heights[start - 1] <= budget {
start -= 1;
used += heights[start];
}
start
}
#[derive(Copy, Clone)]
pub(crate) struct RowPaint {
pub ink: Rgba,
pub ground: Rgba,
pub muted: Rgba,
pub accent: Rgba,
pub danger: Rgba,
pub sel_fg: Rgba,
pub sel_bg: Rgba,
}
pub(crate) struct RowSpec {
pub height: i32,
pub glyph: &'static str,
pub label: String,
pub detail: Option<String>,
pub key: Option<char>,
pub highlighted: bool,
pub focused: bool,
pub danger: bool,
pub checked: Option<bool>,
}
pub(crate) fn blank_row() -> View {
Element::new()
.style(LayoutStyle::line(1).min_h(0).shrink(1.0))
.build()
}
pub(crate) fn choice_row(
paint: RowPaint,
spec: RowSpec,
on_press: impl Fn(&mut EventCtx) + 'static,
) -> View {
let RowSpec {
height,
glyph,
label,
detail,
key,
highlighted,
focused,
danger,
checked,
} = spec;
let mut el = Element::new()
.style(
LayoutStyle::default()
.width(Dimension::Percent(1.0))
.height(Dimension::Cells(height))
.shrink(0.0),
)
.role(if checked.is_some() {
Role::Checkbox
} else {
Role::MenuItem
})
.access_label(label.clone())
.on(Phase::Bubble, move |ctx, ev| {
if let UiEvent::Mouse(m) = ev {
if matches!(m.kind, MouseKind::Down(MouseButton::Left)) {
ctx.stop_propagation();
on_press(ctx);
}
}
});
if let Some(on) = checked {
el = el.access_value(move || if on { "on" } else { "off" }.into());
} else if highlighted {
el = el.access_value(|| "selected".into());
}
el.draw(move |canvas, rect| {
if rect.is_empty() {
return;
}
let selected_pair = highlighted && focused;
let base_ink = if danger { paint.danger } else { paint.ink };
let (glyph_ink, label_ink, bg) = if selected_pair {
(paint.sel_fg, paint.sel_fg, paint.sel_bg)
} else if highlighted {
(
paint.accent,
if danger { paint.danger } else { paint.accent },
paint.ground,
)
} else {
(base_ink, base_ink, paint.ground)
};
let row_style = Style::new().fg(label_ink).bg(bg);
canvas.fill_styled(rect, ' ', &row_style);
canvas.print_styled(rect.origin(), glyph, &Style::new().fg(glyph_ink).bg(bg));
let mut room = (rect.w - GLYPH_W).max(0);
let key_text = key.map(|k| format!("({k})"));
if let Some(kt) = &key_text {
let kw = crate::text::width(kt) + 1;
if room > kw {
room -= kw;
}
}
let shown = crate::text::truncate_ellipsis(&label, room);
canvas.print_styled(Point::new(rect.x + GLYPH_W, rect.y), &shown, &row_style);
if let Some(kt) = &key_text {
let kx = rect.x + GLYPH_W + crate::text::width(&shown) + 1;
if kx + crate::text::width(kt) <= rect.right() {
let key_ink = if selected_pair {
paint.sel_fg
} else {
paint.muted
};
canvas.print_styled(Point::new(kx, rect.y), kt, &Style::new().fg(key_ink).bg(bg));
}
}
if rect.h > 1 {
if let Some(detail) = &detail {
let ink = if selected_pair {
paint.sel_fg
} else {
paint.muted
};
canvas.print_styled(
Point::new(rect.x + GLYPH_W, rect.y + 1),
&crate::text::truncate_ellipsis(detail, (rect.w - GLYPH_W).max(0)),
&Style::new().fg(ink).bg(bg),
);
}
}
})
.build()
}