use std::cell::{Cell, RefCell};
use std::rc::Rc;
use crate::reactive::Scope;
use super::overlays::Overlays;
use super::popups::Modal;
use super::viewport::use_viewport;
#[path = "choice_prompt_interact.rs"]
mod interact;
#[path = "choice_prompt_parts.rs"]
mod parts;
#[path = "choice_prompt_view.rs"]
mod view;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ChoiceOption {
pub id: String,
pub label: String,
pub detail: Option<String>,
pub key: Option<char>,
pub danger: bool,
}
impl ChoiceOption {
pub fn new(id: impl Into<String>, label: impl Into<String>) -> ChoiceOption {
ChoiceOption {
id: id.into(),
label: label.into(),
detail: None,
key: None,
danger: false,
}
}
pub fn detail(mut self, detail: impl Into<String>) -> ChoiceOption {
self.detail = Some(detail.into());
self
}
pub fn key(mut self, key: char) -> ChoiceOption {
self.key = Some(key);
self
}
pub fn danger(mut self, danger: bool) -> ChoiceOption {
self.danger = danger;
self
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ChoiceQuestion {
pub prompt: String,
pub options: Vec<ChoiceOption>,
pub allow_multiple: bool,
pub other: Option<String>,
}
impl ChoiceQuestion {
pub fn new(prompt: impl Into<String>) -> ChoiceQuestion {
ChoiceQuestion {
prompt: prompt.into(),
options: Vec::new(),
allow_multiple: false,
other: None,
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ChoiceAnswer {
pub selected: Vec<String>,
pub other: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ChoiceOutcome {
Answered(ChoiceAnswer),
Cancelled,
}
type ResolveFn = Box<dyn FnOnce(ChoiceOutcome)>;
type BodyFn = Box<dyn FnOnce(Scope) -> crate::ui::View>;
pub struct ChoicePrompt {
question: ChoiceQuestion,
initial: Option<String>,
checked: Vec<String>,
max_visible: i32,
dismissable: bool,
dismiss_label: Option<String>,
overlays: Option<Overlays>,
on_resolve: Option<ResolveFn>,
body: Option<BodyFn>,
body_rows: i32,
body_width: Option<i32>,
}
impl ChoicePrompt {
pub fn new(prompt: impl Into<String>) -> ChoicePrompt {
ChoicePrompt::of(ChoiceQuestion::new(prompt))
}
pub fn of(question: ChoiceQuestion) -> ChoicePrompt {
ChoicePrompt {
question,
initial: None,
checked: Vec::new(),
max_visible: 10,
dismissable: true,
dismiss_label: None,
overlays: None,
on_resolve: None,
body: None,
body_rows: 8,
body_width: None,
}
}
pub fn option(mut self, id: impl Into<String>, label: impl Into<String>) -> ChoicePrompt {
self.question.options.push(ChoiceOption::new(id, label));
self
}
pub fn option_detail(
mut self,
id: impl Into<String>,
label: impl Into<String>,
detail: impl Into<String>,
) -> ChoicePrompt {
self.question
.options
.push(ChoiceOption::new(id, label).detail(detail));
self
}
pub fn option_key(
mut self,
id: impl Into<String>,
label: impl Into<String>,
key: char,
) -> ChoicePrompt {
self.question
.options
.push(ChoiceOption::new(id, label).key(key));
self
}
pub fn option_with(mut self, option: ChoiceOption) -> ChoicePrompt {
self.question.options.push(option);
self
}
pub fn danger(mut self, id: &str) -> ChoicePrompt {
match self.question.options.iter_mut().find(|o| o.id == id) {
Some(o) => o.danger = true,
None => debug_assert!(false, "ChoicePrompt::danger: no option with id {id:?}"),
}
self
}
pub fn dismissable(mut self, dismissable: bool) -> ChoicePrompt {
self.dismissable = dismissable;
self
}
pub fn dismiss_label(mut self, label: impl Into<String>) -> ChoicePrompt {
let label = label.into();
debug_assert!(
!label.trim().is_empty(),
"ChoicePrompt::dismiss_label: an empty label hides a consent affordance"
);
self.dismiss_label = Some(label);
self
}
pub fn allow_multiple(mut self, on: bool) -> ChoicePrompt {
self.question.allow_multiple = on;
self
}
pub fn allow_other(mut self, label: impl Into<String>) -> ChoicePrompt {
self.question.other = Some(label.into());
self
}
pub fn initial(mut self, id: impl Into<String>) -> ChoicePrompt {
self.initial = Some(id.into());
self
}
pub fn checked<I, S>(mut self, ids: I) -> ChoicePrompt
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.checked = ids.into_iter().map(Into::into).collect();
self
}
pub fn max_visible(mut self, rows: i32) -> ChoicePrompt {
self.max_visible = rows.max(1);
self
}
pub fn body(mut self, body: impl FnOnce(Scope) -> crate::ui::View + 'static) -> ChoicePrompt {
self.body = Some(Box::new(body));
self
}
pub fn body_rows(mut self, rows: i32) -> ChoicePrompt {
self.body_rows = rows.max(1);
self
}
pub fn body_width(mut self, cols: i32) -> ChoicePrompt {
self.body_width = Some(cols.max(1));
self
}
pub fn overlays(mut self, overlays: &Overlays) -> ChoicePrompt {
self.overlays = Some(overlays.clone());
self
}
pub fn on_resolve(mut self, f: impl FnOnce(ChoiceOutcome) + 'static) -> ChoicePrompt {
self.on_resolve = Some(Box::new(f));
self
}
pub fn open(self, cx: Scope) -> ChoicePromptHandle {
let overlays = self
.overlays
.clone()
.or_else(|| cx.use_context::<Overlays>());
let resolved = Rc::new(Cell::new(false));
let cb_slot: Rc<RefCell<Option<ResolveFn>>> = Rc::new(RefCell::new(self.on_resolve));
let modal_slot: Rc<RefCell<Option<Modal>>> = Rc::new(RefCell::new(None));
let finish: Rc<dyn Fn() -> Option<ResolveFn>> = {
let resolved = resolved.clone();
let cb_slot = cb_slot.clone();
let modal_slot = modal_slot.clone();
Rc::new(move || {
if resolved.replace(true) {
return None;
}
let modal = modal_slot.borrow_mut().take();
if let Some(modal) = modal {
modal.close();
}
cb_slot.borrow_mut().take()
})
};
let resolve: Rc<dyn Fn(ChoiceOutcome)> = {
let finish = finish.clone();
Rc::new(move |outcome: ChoiceOutcome| {
if let Some(cb) = finish() {
cb(outcome);
}
})
};
let handle = ChoicePromptHandle {
resolved: resolved.clone(),
finish: finish.clone(),
};
let Some(overlays) = overlays else {
debug_assert!(
false,
"ChoicePrompt: no Overlays available — open inside an App (context) \
or pass .overlays(..) explicitly"
);
resolve(ChoiceOutcome::Cancelled);
return handle;
};
if self.question.options.is_empty() && self.question.other.is_none() {
debug_assert!(
false,
"ChoicePrompt: a question with no options and no Other choice \
cannot be answered"
);
resolve(ChoiceOutcome::Cancelled);
return handle;
}
let viewport = use_viewport(cx).get_untracked();
let tokens = crate::widgets::theme_tokens(cx);
let question = Rc::new(self.question);
let geo = parts::measure(
&question,
viewport,
self.max_visible,
self.dismissable,
self.dismiss_label.as_deref(),
self.body.as_ref().map(|_| self.body_rows),
self.body.as_ref().and(self.body_width),
);
let initial_row = self
.initial
.as_deref()
.and_then(|id| question.options.iter().position(|o| o.id == id))
.unwrap_or(0);
let initial_checked: Vec<bool> = if question.allow_multiple {
question
.options
.iter()
.map(|o| self.checked.contains(&o.id))
.collect()
} else {
vec![false; question.options.len()]
};
let panel = geo.panel;
let spec_resolve = resolve.clone();
let dismissable = self.dismissable;
let dismiss_label = self.dismiss_label;
let body = self.body;
let modal = Modal::open(&overlays, cx, viewport, panel, move |mcx| {
view::gate_content(
mcx,
tokens,
view::GateSpec {
question,
geo,
initial_row,
initial_checked,
dismissable,
dismiss_label,
resolve: spec_resolve,
body,
},
)
});
*modal_slot.borrow_mut() = Some(modal);
handle
}
}
#[derive(Clone)]
pub struct ChoicePromptHandle {
resolved: Rc<Cell<bool>>,
finish: Rc<dyn Fn() -> Option<ResolveFn>>,
}
impl ChoicePromptHandle {
pub fn cancel(&self) {
if let Some(cb) = (self.finish)() {
cb(ChoiceOutcome::Cancelled);
}
}
pub fn retire(&self) {
drop((self.finish)());
}
pub fn is_open(&self) -> bool {
!self.resolved.get()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ChoiceSequenceOutcome {
Completed(Vec<ChoiceAnswer>),
Cancelled {
index: usize,
answers: Vec<ChoiceAnswer>,
},
}
pub struct ChoiceSequence {
questions: Vec<ChoiceQuestion>,
overlays: Option<Overlays>,
on_resolve: Option<Box<dyn FnOnce(ChoiceSequenceOutcome)>>,
}
impl ChoiceSequence {
pub fn new(questions: Vec<ChoiceQuestion>) -> ChoiceSequence {
ChoiceSequence {
questions,
overlays: None,
on_resolve: None,
}
}
pub fn overlays(mut self, overlays: &Overlays) -> ChoiceSequence {
self.overlays = Some(overlays.clone());
self
}
pub fn on_resolve(mut self, f: impl FnOnce(ChoiceSequenceOutcome) + 'static) -> ChoiceSequence {
self.on_resolve = Some(Box::new(f));
self
}
pub fn open(self, cx: Scope) {
let done = self
.on_resolve
.unwrap_or_else(|| Box::new(|_| {}) as Box<dyn FnOnce(ChoiceSequenceOutcome)>);
if self.questions.is_empty() {
done(ChoiceSequenceOutcome::Completed(Vec::new()));
return;
}
advance(
cx,
self.overlays,
Rc::new(self.questions),
0,
Vec::new(),
done,
);
}
}
fn advance(
cx: Scope,
overlays: Option<Overlays>,
questions: Rc<Vec<ChoiceQuestion>>,
index: usize,
answers: Vec<ChoiceAnswer>,
done: Box<dyn FnOnce(ChoiceSequenceOutcome)>,
) {
let mut prompt = ChoicePrompt::of(questions[index].clone());
if let Some(o) = &overlays {
prompt = prompt.overlays(o);
}
prompt
.on_resolve(move |outcome| match outcome {
ChoiceOutcome::Answered(answer) => {
let mut answers = answers;
answers.push(answer);
if index + 1 == questions.len() {
done(ChoiceSequenceOutcome::Completed(answers));
} else {
advance(cx, overlays, questions, index + 1, answers, done);
}
}
ChoiceOutcome::Cancelled => done(ChoiceSequenceOutcome::Cancelled { index, answers }),
})
.open(cx);
}
#[cfg(test)]
#[path = "choice_prompt_tests.rs"]
mod tests;