use std::cell::RefCell;
use std::rc::Rc;
use crate::layout::{Dimension, Style as LayoutStyle};
use crate::reactive::{Scope, Signal};
use crate::ui::{Element, EventCtx, Key, Mods, MouseButton, MouseKind, Phase, UiEvent, View};
type HeightFn = Box<dyn Fn(usize) -> i32>;
pub(crate) fn prefix_sums(len: usize, height: impl Fn(usize) -> i32) -> Vec<i32> {
let mut out = Vec::with_capacity(len + 1);
let mut acc = 0i32;
out.push(0);
for i in 0..len {
acc += height(i).max(1);
out.push(acc);
}
out
}
pub(crate) fn nav_target(key: Key, cur: usize, len: usize, page: usize) -> Option<usize> {
Some(match key {
Key::Up => cur.saturating_sub(1),
Key::Down => cur + 1,
Key::PageUp => cur.saturating_sub(page),
Key::PageDown => cur + page,
Key::Home => 0,
Key::End => len.saturating_sub(1),
_ => return None,
})
}
#[derive(Clone)]
pub(crate) struct SelectionModel {
pub(crate) len: usize,
pub(crate) keys: Option<Rc<Vec<String>>>,
pub(crate) prefix: Rc<Vec<i32>>,
pub(crate) selection: Signal<usize>,
pub(crate) selection_key: Option<Signal<String>>,
pub(crate) offset: Signal<i32>,
}
impl SelectionModel {
pub(crate) fn total_rows(&self) -> i32 {
*self.prefix.last().unwrap_or(&0)
}
pub(crate) fn row_at(&self, row: i32) -> Option<usize> {
if row < 0 || row >= self.total_rows() {
return None;
}
let idx = self.prefix.partition_point(|&p| p <= row).saturating_sub(1);
(idx < self.len).then_some(idx)
}
pub(crate) fn ensure_visible(&self, idx: usize, view_h: i32) {
let total_rows = self.total_rows();
let top = self.prefix[idx];
let bottom = self.prefix[idx + 1];
self.offset.update(|o| {
if top < *o {
*o = top;
}
if view_h > 0 && bottom > *o + view_h {
*o = bottom - view_h;
}
*o = (*o).clamp(0, (total_rows - view_h.max(1)).max(0));
});
}
pub(crate) fn settle(&self) {
if self.len == 0 {
return;
}
let by_key = self
.selection_key
.zip(self.keys.as_ref())
.and_then(|(sig, keys)| {
let wanted = sig.get_untracked();
keys.iter().position(|k| *k == wanted)
});
let idx = by_key.unwrap_or_else(|| self.selection.get_untracked().min(self.len - 1));
self.selection.set_if_changed(idx);
if let (Some(sig), Some(keys)) = (self.selection_key, self.keys.as_ref()) {
sig.set_if_changed(keys[idx].clone());
}
}
pub(crate) fn select(&self, target: usize, view_h: i32) -> bool {
if self.len == 0 {
return false; }
let target = target.min(self.len - 1);
let changed = self.selection.get_untracked() != target;
if changed {
self.selection.set(target);
if let (Some(key_sig), Some(keys)) = (self.selection_key, self.keys.as_ref()) {
if let Some(k) = keys.get(target) {
key_sig.set(k.clone());
}
}
}
self.ensure_visible(target, view_h);
changed
}
}
pub struct RowSelect {
keys: Vec<String>,
heights: Option<HeightFn>,
selection: Option<Signal<usize>>,
selection_key: Option<Signal<String>>,
offset_y: Option<Signal<i32>>,
scroll_to: Option<Signal<Option<usize>>>,
focused: Option<Signal<bool>>,
focusable: bool,
layout: Option<LayoutStyle>,
on_select: Option<Box<dyn FnMut(usize)>>,
on_activate: Option<Box<dyn FnMut(usize)>>,
}
impl RowSelect {
pub fn new<I, S>(keys: I) -> RowSelect
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
RowSelect {
keys: keys.into_iter().map(Into::into).collect(),
heights: None,
selection: None,
selection_key: None,
offset_y: None,
scroll_to: None,
focused: None,
focusable: true,
layout: None,
on_select: None,
on_activate: None,
}
}
pub fn row_heights(mut self, f: impl Fn(usize) -> i32 + 'static) -> RowSelect {
self.heights = Some(Box::new(f));
self
}
pub fn selection(mut self, selection: Signal<usize>) -> RowSelect {
self.selection = Some(selection);
self
}
pub fn selection_key(mut self, key: Signal<String>) -> RowSelect {
self.selection_key = Some(key);
self
}
pub fn offset_y(mut self, offset: Signal<i32>) -> RowSelect {
self.offset_y = Some(offset);
self
}
pub fn scroll_to(mut self, request: Signal<Option<usize>>) -> RowSelect {
self.scroll_to = Some(request);
self
}
pub fn focus_signal(mut self, focused: Signal<bool>) -> RowSelect {
self.focused = Some(focused);
self
}
pub fn focusable(mut self, focusable: bool) -> RowSelect {
self.focusable = focusable;
self
}
pub fn layout(mut self, layout: LayoutStyle) -> RowSelect {
self.layout = Some(layout);
self
}
pub fn on_select(mut self, f: impl FnMut(usize) + 'static) -> RowSelect {
self.on_select = Some(Box::new(f));
self
}
pub fn on_activate(mut self, f: impl FnMut(usize) + 'static) -> RowSelect {
self.on_activate = Some(Box::new(f));
self
}
pub fn wrap(self, cx: Scope, content: View) -> Element {
let len = self.keys.len();
let keys = Rc::new(self.keys);
let heights = self.heights;
let prefix = Rc::new(prefix_sums(len, |i| {
heights.as_ref().map(|f| f(i)).unwrap_or(1)
}));
let selection = self.selection.unwrap_or_else(|| cx.signal(0usize));
let offset = self.offset_y.unwrap_or_else(|| cx.signal(0i32));
let model = SelectionModel {
len,
keys: Some(keys),
prefix,
selection,
selection_key: self.selection_key,
offset,
};
let total_rows = model.total_rows();
model.settle();
if self.offset_y.is_some() {
offset.update(|o| *o = (*o).clamp(0, (total_rows - 1).max(0)));
}
let on_select: crate::widgets::SharedCallback<usize> =
Rc::new(RefCell::new(self.on_select));
let on_activate: crate::widgets::SharedCallback<usize> =
Rc::new(RefCell::new(self.on_activate));
let select = {
let model = model.clone();
let on_select = on_select.clone();
move |target: usize, view_h: i32| {
if model.select(target, view_h) {
if let Some(f) = on_select.borrow_mut().as_mut() {
f(target);
}
}
}
};
let view_box = cx.signal((0i32, 0i32));
if let Some(request) = self.scroll_to {
let model = model.clone();
cx.effect_labeled("row-select-scroll-to", move || {
let Some(idx) = request.get() else {
return;
};
if model.len == 0 {
request.set(None);
return;
}
let vh = view_box.get().1;
if vh <= 0 {
return; }
model.ensure_visible(idx.min(model.len - 1), vh);
request.set(None); });
}
let keys_model = model.clone();
let key_activate = on_activate.clone();
let key_select = select.clone();
let key_handler = move |ctx: &mut EventCtx, ev: &UiEvent| {
let UiEvent::Key(k) = ev else { return };
if len == 0 || k.mods != Mods::NONE {
return;
}
if matches!(k.key, Key::Enter | Key::Char(' ')) {
if let Some(f) = key_activate.borrow_mut().as_mut() {
f(keys_model.selection.get_untracked().min(len - 1));
ctx.stop_propagation();
}
return;
}
let h = ctx.current_rect().h.max(1);
let cur = keys_model.selection.get_untracked();
let Some(target) = nav_target(k.key, cur, len, (h as usize).max(1)) else {
return;
};
key_select(target, h);
ctx.stop_propagation();
};
let mouse_model = model.clone();
let mouse_activate = on_activate;
let mouse_handler = move |ctx: &mut EventCtx, ev: &UiEvent| {
let UiEvent::Mouse(m) = ev else { return };
if !matches!(m.kind, MouseKind::Down(MouseButton::Left)) {
return;
}
let rect = ctx.current_rect();
let row = m.pos.y - rect.y + mouse_model.offset.get_untracked();
let Some(idx) = mouse_model.row_at(row) else {
return;
};
let was_selected = mouse_model.selection.get_untracked() == idx;
select(idx, rect.h.max(1));
if was_selected {
if let Some(f) = mouse_activate.borrow_mut().as_mut() {
f(idx);
}
}
ctx.stop_propagation();
};
let layout = self
.layout
.unwrap_or_else(|| LayoutStyle::default().grow(1.0));
let mut el = Element::new()
.style(layout)
.role(crate::ui::Role::List)
.access_value(move || {
if len == 0 {
return "0 items".into();
}
format!("{} items, selected {}", len, selection.get_untracked() + 1)
})
.draw(super::scroll::size_probe(cx, view_box));
if self.focusable {
el = el.focusable();
}
if let Some(focused) = self.focused {
el = el.focus_signal(focused);
}
el.on(Phase::Capture, key_handler)
.on(Phase::Bubble, mouse_handler)
.child(
Element::new()
.style(
LayoutStyle::default()
.width(Dimension::Percent(1.0))
.height(Dimension::Percent(1.0)),
)
.child(content)
.build(),
)
}
}
#[cfg(test)]
#[path = "row_select_tests.rs"]
mod tests;