use std::cell::RefCell;
use std::rc::Rc;
use crate::base::{Rect, Size};
use crate::layout::{Dimension, Style as LayoutStyle};
use crate::reactive::{Scope, Signal};
use crate::render::Style;
use crate::theme::TokenSet;
use crate::ui::{Element, EventCtx, Key, Mods, MouseButton, MouseKind, Phase, Role, UiEvent};
use super::anchored::{DismissReason, PanelAnchor, PanelWidth, Popup};
use super::overlays::Overlays;
use super::viewport::use_viewport;
#[path = "select_core.rs"]
pub(crate) mod core;
use core::{
first_enabled, last_enabled, option_rows_view, page_highlight, resolve_overlays,
step_highlight, trigger_view, type_ahead_target, OptionRows, TriggerLabel, TypeAhead,
DEFAULT_MAX_VISIBLE,
};
#[path = "select_combobox.rs"]
mod combobox;
pub use combobox::Combobox;
#[path = "select_handle.rs"]
mod handle;
use handle::anchor_cell;
pub use handle::SelectHandle;
#[path = "select_multi.rs"]
mod multi;
pub use multi::MultiSelect;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SelectOption {
pub key: String,
pub label: String,
pub hint: Option<String>,
pub disabled: bool,
}
impl SelectOption {
pub fn new(label: impl Into<String>) -> SelectOption {
let label = label.into();
SelectOption {
key: label.clone(),
label,
hint: None,
disabled: false,
}
}
pub fn keyed(key: impl Into<String>, label: impl Into<String>) -> SelectOption {
SelectOption {
key: key.into(),
label: label.into(),
hint: None,
disabled: false,
}
}
pub fn hint(mut self, hint: impl Into<String>) -> SelectOption {
self.hint = Some(hint.into());
self
}
pub fn disabled(mut self, disabled: bool) -> SelectOption {
self.disabled = disabled;
self
}
}
struct SelectSession {
popup: Option<Popup>,
pre_open: usize,
type_ahead: TypeAhead,
}
pub struct Select {
options: Vec<SelectOption>,
value: Option<Signal<usize>>,
placeholder: String,
disabled: bool,
commit_on_move: bool,
max_visible: usize,
layout: Option<LayoutStyle>,
overlays: Option<Overlays>,
on_change: Option<Box<dyn FnMut(usize)>>,
handle: Option<SelectHandle>,
}
impl Select {
pub fn new(options: Vec<SelectOption>) -> Select {
Select {
options,
value: None,
placeholder: String::from("choose…"),
disabled: false,
commit_on_move: false,
max_visible: DEFAULT_MAX_VISIBLE,
layout: None,
overlays: None,
on_change: None,
handle: None,
}
}
pub fn value(mut self, value: Signal<usize>) -> Select {
self.value = Some(value);
self
}
pub fn placeholder(mut self, text: impl Into<String>) -> Select {
self.placeholder = text.into();
self
}
pub fn disabled(mut self, disabled: bool) -> Select {
self.disabled = disabled;
self
}
pub fn commit_on_move(mut self, on: bool) -> Select {
self.commit_on_move = on;
self
}
pub fn max_visible(mut self, n: usize) -> Select {
self.max_visible = n.max(1);
self
}
pub fn layout(mut self, layout: LayoutStyle) -> Select {
self.layout = Some(layout);
self
}
pub fn overlays(mut self, overlays: &Overlays) -> Select {
self.overlays = Some(overlays.clone());
self
}
pub fn on_change(mut self, f: impl FnMut(usize) + 'static) -> Select {
self.on_change = Some(Box::new(f));
self
}
pub fn handle(mut self, handle: &SelectHandle) -> Select {
self.handle = Some(handle.clone());
self
}
pub fn view(self, cx: Scope) -> crate::ui::View {
let t = crate::widgets::theme_tokens(cx);
self.element(cx, &t).build()
}
pub fn element(self, cx: Scope, t: &TokenSet) -> Element {
let tokens = *t;
let options: Rc<Vec<SelectOption>> = Rc::new(self.options);
let value = self.value.unwrap_or_else(|| cx.signal(usize::MAX));
let placeholder = self.placeholder;
let disabled = self.disabled;
let commit_on_move = self.commit_on_move;
let max_visible = self.max_visible;
let overlays = resolve_overlays(cx, self.overlays);
let viewport = use_viewport(cx);
let on_change: crate::widgets::SharedCallback<usize> =
Rc::new(RefCell::new(self.on_change));
let focused = cx.signal(false);
let hovered = cx.signal(false);
let display: Signal<Vec<usize>> = cx.signal((0..options.len()).collect());
let highlight: Signal<usize> = cx.signal(0);
let session: Rc<RefCell<SelectSession>> = Rc::new(RefCell::new(SelectSession {
popup: None,
pre_open: usize::MAX,
type_ahead: TypeAhead::default(),
}));
let write_value = Rc::new({
let on_change = on_change.clone();
move |idx: usize| {
if value.get_untracked() == idx {
return;
}
value.set(idx);
if let Some(f) = on_change.borrow_mut().as_mut() {
f(idx);
}
}
});
let commit_and_close = Rc::new({
let session = session.clone();
let options = options.clone();
let write_value = write_value.clone();
move || {
let disp = display.get_untracked();
let pos = highlight.get_untracked().min(disp.len().saturating_sub(1));
let Some(&idx) = disp.get(pos) else { return };
if options.get(idx).is_some_and(|o| o.disabled) {
return;
}
write_value(idx);
let popup = session.borrow().popup.clone();
if let Some(popup) = popup {
popup.dismiss(DismissReason::Commit);
}
}
});
let open = Rc::new({
let session = session.clone();
let options = options.clone();
let write_value = write_value.clone();
let commit_and_close = commit_and_close.clone();
let overlays = overlays.clone();
move |anchor: Rect| {
if session.borrow().popup.is_some() {
return;
}
let Some(overlays) = overlays.clone() else {
debug_assert!(
false,
"Select: no Overlays available — build inside an App (context) \
or pass .overlays(..) explicitly"
);
return;
};
let disp = display.get_untracked();
if disp.is_empty() {
return;
}
let current = value.get_untracked();
let seed = disp
.iter()
.position(|&i| i == current)
.filter(|&p| options.get(disp[p]).is_some_and(|o| !o.disabled))
.or_else(|| first_enabled(&options, &disp))
.unwrap_or(0);
highlight.set(seed);
{
let mut s = session.borrow_mut();
s.pre_open = current;
s.type_ahead.clear();
}
let rows = disp.len().min(max_visible) as i32;
let build = select_popup_content(
tokens,
options.clone(),
display,
highlight,
max_visible,
commit_on_move,
session.clone(),
write_value.clone(),
commit_and_close.clone(),
);
let popup = Popup::open(
&overlays,
cx,
viewport.get_untracked(),
PanelAnchor { rect: anchor },
PanelWidth::MatchAnchor,
Size::new(anchor.w, rows),
build,
);
let Some(popup) = popup else { return };
popup.on_dismiss({
let session = session.clone();
let write_value = write_value.clone();
move |reason| {
let pre_open = session.borrow().pre_open;
if reason == DismissReason::Escape && commit_on_move {
write_value(pre_open);
}
session.borrow_mut().popup = None;
}
});
session.borrow_mut().popup = Some(popup);
}
});
let last_rect = anchor_cell();
if let Some(h) = &self.handle {
if !disabled {
let open = open.clone();
let session = session.clone();
let last_rect = last_rect.clone();
h.wire(cx, move || {
if session.borrow().popup.is_some() {
return true; }
let Some(anchor) = last_rect.get() else {
return false; };
open(anchor);
session.borrow().popup.is_some()
});
}
}
let access_options = options.clone();
let access_placeholder = placeholder.clone();
let mut el = Element::new()
.style(self.layout.unwrap_or_else(|| {
LayoutStyle::default()
.height(Dimension::Cells(1))
.grow(1.0)
.shrink(0.0)
}))
.role(Role::Button)
.access_label(placeholder.clone())
.access_value(move || {
let v = value.get_untracked();
access_options
.get(v)
.map(|o| o.label.clone())
.unwrap_or_else(|| access_placeholder.clone())
})
.draw(move |_canvas, rect| {
let o = crate::ui::layer_origin();
last_rect.set(Some(rect.translate(o.x, o.y)));
})
.hover_signal(hovered)
.focus_signal(focused);
if !disabled {
let open = open.clone();
el = el.focusable().on(Phase::Bubble, move |ctx, ev| match ev {
UiEvent::Key(k)
if (k.key == Key::Enter || k.key == Key::Char(' ')) && k.mods == Mods::NONE =>
{
if focused.get_untracked() {
open(ctx.current_rect_screen());
ctx.stop_propagation();
}
}
UiEvent::Mouse(m) if matches!(m.kind, MouseKind::Down(MouseButton::Left)) => {
open(ctx.current_rect_screen());
ctx.stop_propagation();
}
_ => {}
});
}
let label_options = options.clone();
let label_placeholder = placeholder.clone();
el.child(trigger_view(
t,
focused,
hovered,
disabled,
Rc::new(move || {
let v = value.get();
match label_options.get(v) {
Some(o) => TriggerLabel {
text: o.label.clone(),
short: None,
placeholder: false,
},
None => TriggerLabel {
text: label_placeholder.clone(),
short: None,
placeholder: true,
},
}
}),
))
}
}
#[allow(clippy::too_many_arguments)]
fn select_popup_content(
tokens: TokenSet,
options: Rc<Vec<SelectOption>>,
display: Signal<Vec<usize>>,
highlight: Signal<usize>,
max_visible: usize,
commit_on_move: bool,
session: Rc<RefCell<SelectSession>>,
write_value: Rc<dyn Fn(usize)>,
commit_and_close: Rc<dyn Fn()>,
) -> impl FnOnce(Scope, bool) -> crate::ui::View {
move |_pcx: Scope, _flipped: bool| {
let ground = tokens.surface_raised;
let ink = tokens.text;
let move_to = Rc::new({
let options = options.clone();
move |pos: usize| {
highlight.set(pos);
if commit_on_move {
if let Some(&idx) = display.get_untracked().get(pos) {
if options.get(idx).is_some_and(|o| !o.disabled) {
write_value(idx);
}
}
}
}
});
let key_handler = {
let options = options.clone();
let session = session.clone();
let commit = commit_and_close.clone();
let move_to = move_to.clone();
move |ctx: &mut EventCtx, ev: &UiEvent| {
let UiEvent::Key(k) = ev else { return };
if k.mods != Mods::NONE {
return;
}
let disp = display.get_untracked();
let h = highlight.get_untracked().min(disp.len().saturating_sub(1));
match k.key {
Key::Down => move_to(step_highlight(&options, &disp, h, 1)),
Key::Up => move_to(step_highlight(&options, &disp, h, -1)),
Key::Home => {
if let Some(p) = first_enabled(&options, &disp) {
move_to(p);
}
}
Key::End => {
if let Some(p) = last_enabled(&options, &disp) {
move_to(p);
}
}
Key::PageDown => move_to(page_highlight(&options, &disp, h, 1, max_visible)),
Key::PageUp => move_to(page_highlight(&options, &disp, h, -1, max_visible)),
Key::Enter => commit(),
Key::Char(c) if !c.is_control() => {
let now = crate::ui::event_time().unwrap_or_else(std::time::Instant::now);
let target = {
let mut s = session.borrow_mut();
let buf = s.type_ahead.push(c, now);
type_ahead_target(&options, &disp, buf, h)
};
if let Some(p) = target {
move_to(p);
}
}
_ => return, }
ctx.stop_propagation();
}
};
let on_activate = Rc::new({
let commit = commit_and_close.clone();
move |pos: usize| {
highlight.set(pos);
commit();
}
});
Element::new()
.style(
LayoutStyle::column()
.width(Dimension::Percent(1.0))
.height(Dimension::Percent(1.0)),
)
.role(Role::Menu)
.access_label("options")
.on(Phase::Bubble, key_handler)
.draw(move |canvas, rect| {
canvas.fill_styled(rect, ' ', &Style::new().fg(ink).bg(ground));
})
.child(option_rows_view(
&tokens,
OptionRows {
options,
display,
highlight,
checks: None,
max_visible,
on_activate,
},
))
.build()
}
}
#[cfg(test)]
#[path = "select_tests.rs"]
mod tests;