use std::cell::Cell;
use std::rc::Rc;
use crate::base::{Point, Rgba};
use crate::layout::{Dimension, Style as LayoutStyle};
use crate::reactive::{Memo, Signal};
use crate::render::Style;
use crate::ui::{dyn_view, Element, EventCtx, Key, Mods, MouseKind, UiEvent, View, ViewId};
use super::parts::{clamp_row, hint_segments};
#[derive(Copy, Clone)]
pub(crate) struct GateState {
pub highlight: Signal<usize>,
pub checks: Signal<Vec<bool>>,
pub other_on: Signal<bool>,
pub other_text: Signal<String>,
pub needs_other: Signal<bool>,
pub esc_note: Signal<bool>,
pub editing: Signal<bool>,
pub region_focused: Signal<bool>,
}
#[derive(Copy, Clone)]
pub(crate) struct GateDims {
pub n: usize,
pub rows_total: usize,
pub other_row: usize,
pub multiple: bool,
pub has_other: bool,
pub dismissable: bool,
pub windowed: bool,
}
#[derive(Clone, Default)]
pub(crate) struct Anchors {
pub region: Rc<Cell<Option<ViewId>>>,
pub root: Rc<Cell<Option<ViewId>>>,
}
impl Anchors {
pub(crate) fn retreat(&self, ctx: &mut EventCtx) {
if let Some(a) = self.region.get().or_else(|| self.root.get()) {
ctx.request_focus(a);
}
}
}
pub(crate) fn move_row(s: GateState, d: GateDims, row: usize) {
s.esc_note.set(false);
s.highlight.set(clamp_row(row, d.rows_total));
}
pub(crate) fn toggle_row(s: GateState, d: GateDims, row: usize) {
s.esc_note.set(false);
if row < d.n {
s.checks.update(|c| {
if let Some(m) = c.get_mut(row) {
*m = !*m;
}
});
} else {
s.other_on.update(|b| *b = !*b);
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn root_key_handler(
s: GateState,
d: GateDims,
letters: Rc<Vec<(char, usize)>>,
try_commit: Rc<dyn Fn()>,
anchors: Anchors,
) -> impl FnMut(&mut EventCtx, &UiEvent) {
let retreat_guard = {
let anchors = anchors.clone();
move |ctx: &mut EventCtx, from: usize, to: usize| {
if !d.multiple && d.has_other && from == d.other_row && to != d.other_row {
anchors.retreat(ctx);
}
}
};
move |ctx: &mut EventCtx, ev: &UiEvent| match ev {
UiEvent::Key(k) => {
let k = k.normalized();
if k.mods != Mods::NONE {
return;
}
let h = s.highlight.get_untracked();
match k.key {
Key::Down => {
retreat_guard(ctx, h, clamp_row(h + 1, d.rows_total));
move_row(s, d, h + 1);
}
Key::Up => {
retreat_guard(ctx, h, h.saturating_sub(1));
move_row(s, d, h.saturating_sub(1));
}
Key::Home => {
retreat_guard(ctx, h, 0);
move_row(s, d, 0);
}
Key::End => move_row(s, d, d.rows_total.saturating_sub(1)),
Key::Enter => {
ctx.stop_propagation();
try_commit();
return;
}
Key::Escape if !d.dismissable => {
s.esc_note.set(true);
}
Key::Char(c) if !c.is_control() => {
if let Some(&(_, row)) = letters.iter().find(|(key, _)| *key == c) {
if d.multiple {
move_row(s, d, row);
toggle_row(s, d, row);
} else {
move_row(s, d, row);
ctx.stop_propagation();
try_commit();
return;
}
} else if c == ' ' && d.multiple {
toggle_row(s, d, h);
} else if let Some(digit) = c.to_digit(10).filter(|d| (1..=9).contains(d)) {
let row = digit as usize - 1;
if row >= d.n {
return;
}
move_row(s, d, row);
if d.multiple {
toggle_row(s, d, row);
}
} else {
return;
}
}
_ => return,
}
ctx.stop_propagation();
}
UiEvent::Mouse(m) => match m.kind {
MouseKind::ScrollUp => {
let h = s.highlight.get_untracked();
retreat_guard(ctx, h, h.saturating_sub(1));
move_row(s, d, h.saturating_sub(1));
ctx.stop_propagation();
}
MouseKind::ScrollDown => {
let h = s.highlight.get_untracked();
retreat_guard(ctx, h, clamp_row(h + 1, d.rows_total));
move_row(s, d, h + 1);
ctx.stop_propagation();
}
_ => {}
},
_ => {}
}
}
#[derive(Copy, Clone)]
pub(crate) struct HintPaint {
pub muted: Rgba,
pub accent: Rgba,
pub ground: Rgba,
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn hint_row(
s: GateState,
d: GateDims,
paint: HintPaint,
engaged: Memo<bool>,
other_label: String,
keys: String,
esc: Option<String>,
) -> View {
dyn_view(
LayoutStyle::default()
.width(Dimension::Percent(1.0))
.height(Dimension::Cells(1)),
move || {
let warn =
s.needs_other.get() && engaged.get() && s.other_text.with(|t| t.trim().is_empty());
let refused = !warn && s.esc_note.get();
let editing = s.editing.get();
let note = if warn {
Some(format!("{other_label} needs text — type your answer"))
} else if refused {
Some(String::from("an answer is required"))
} else {
None
};
let h = s.highlight.get();
let pos =
(d.windowed && note.is_none() && h < d.n).then(|| format!("{}/{}", h + 1, d.n));
let ink = if note.is_some() {
paint.accent
} else {
paint.muted
};
let segs: Vec<String> = if editing {
vec![
String::from("Enter confirms"),
String::from("Esc back to the list"),
]
} else {
hint_segments(d.multiple, esc.as_deref(), &keys)
};
let bg = paint.ground;
Element::new()
.style(
LayoutStyle::default()
.width(Dimension::Percent(1.0))
.height(Dimension::Cells(1)),
)
.draw(move |canvas, rect| {
if rect.is_empty() {
return;
}
let style = Style::new().fg(ink).bg(bg);
let mut room = rect.w;
if let Some(pos) = &pos {
let pw = crate::text::width(pos);
if pw <= rect.w {
canvas.print_styled(Point::new(rect.right() - pw, rect.y), pos, &style);
room = rect.w - pw - 1;
}
}
let text = match ¬e {
Some(w) => crate::text::truncate_ellipsis(w, room.max(0)),
None => {
let mut segs = segs.clone();
loop {
let joined = segs.join(" · ");
if crate::text::width(&joined) <= room || segs.len() == 1 {
break crate::text::truncate_ellipsis(&joined, room.max(0));
}
segs.remove(0);
}
}
};
canvas.print_styled(rect.origin(), &text, &style);
})
.build()
},
)
}