use std::collections::BTreeSet;
use gpui::prelude::*;
use gpui::{div, px, Context, EventEmitter, FocusHandle, IntoElement, SharedString, Window};
use crate::icon::{Icon, IconName};
use crate::theme::{theme, Size};
#[derive(Debug, Clone)]
pub struct TransferEvent(pub Vec<usize>);
fn move_checked(right: &mut BTreeSet<usize>, checked: &mut BTreeSet<usize>, to_right: bool) -> bool {
let movers: Vec<usize> = checked
.iter()
.copied()
.filter(|i| right.contains(i) != to_right)
.collect();
for i in &movers {
if to_right {
right.insert(*i);
} else {
right.remove(i);
}
checked.remove(i);
}
!movers.is_empty()
}
pub struct Transfer {
items: Vec<SharedString>,
right: BTreeSet<usize>,
checked: BTreeSet<usize>,
titles: (SharedString, SharedString),
height: f32,
focus: FocusHandle,
disabled: bool,
}
impl EventEmitter<TransferEvent> for Transfer {}
impl Transfer {
pub fn new(cx: &mut Context<Self>) -> Self {
Transfer {
items: Vec::new(),
right: BTreeSet::new(),
checked: BTreeSet::new(),
titles: (
SharedString::new_static("Available"),
SharedString::new_static("Chosen"),
),
height: 200.0,
focus: cx.focus_handle(),
disabled: false,
}
}
pub fn data<I, S>(mut self, items: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<SharedString>,
{
self.items = items.into_iter().map(Into::into).collect();
self
}
pub fn chosen(mut self, indices: impl IntoIterator<Item = usize>) -> Self {
self.right = indices.into_iter().collect();
self
}
pub fn titles(
mut self,
left: impl Into<SharedString>,
right: impl Into<SharedString>,
) -> Self {
self.titles = (left.into(), right.into());
self
}
pub fn height(mut self, height: f32) -> Self {
self.height = height.max(80.0);
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn chosen_indices(&self) -> Vec<usize> {
self.right.iter().copied().collect()
}
fn toggle_checked(&mut self, index: usize, cx: &mut Context<Self>) {
if !self.checked.remove(&index) {
self.checked.insert(index);
}
cx.notify();
}
fn transfer(&mut self, to_right: bool, cx: &mut Context<Self>) {
if move_checked(&mut self.right, &mut self.checked, to_right) {
cx.emit(TransferEvent(self.chosen_indices()));
cx.notify();
}
}
fn pane(
&self,
title: &SharedString,
indices: Vec<usize>,
list_id: &'static str,
cx: &mut Context<Self>,
) -> impl IntoElement {
let t = theme(cx);
let radius = t.radius(t.default_radius);
let surface = t.surface().hsla();
let surface_hover = t.surface_hover().hsla();
let border = t.border().hsla();
let text_color = t.text().hsla();
let dimmed = t.dimmed().hsla();
let checked_bg = t.primary().alpha(0.12);
let font = t.font_size(Size::Sm);
let mut rows = div()
.id(list_id)
.flex()
.flex_col()
.gap(px(2.0))
.p(px(4.0))
.flex_1()
.overflow_y_scroll();
for i in indices {
let is_checked = self.checked.contains(&i);
let mut row = div()
.id((list_id, i))
.px(px(8.0))
.py(px(5.0))
.rounded(px(4.0))
.text_size(px(font))
.text_color(text_color)
.child(self.items[i].clone())
.on_click(cx.listener(move |this, _ev, _window, cx| {
if !this.disabled {
this.toggle_checked(i, cx);
}
}));
if is_checked {
row = row.bg(checked_bg);
} else {
row = row.hover(move |s| s.bg(surface_hover));
}
rows = rows.child(row);
}
div()
.flex()
.flex_col()
.flex_1()
.h(px(self.height))
.rounded(px(radius))
.border_1()
.border_color(border)
.bg(surface)
.child(
div()
.px(px(8.0))
.py(px(5.0))
.border_b_1()
.border_color(border)
.text_size(px(font - 1.0))
.text_color(dimmed)
.child(title.clone()),
)
.child(rows)
}
}
impl Render for Transfer {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let t = theme(cx);
let surface = t.surface().hsla();
let surface_hover = t.surface_hover().hsla();
let border = t.border().hsla();
let dimmed = t.dimmed().hsla();
let left: Vec<usize> = (0..self.items.len())
.filter(|i| !self.right.contains(i))
.collect();
let right: Vec<usize> = self.chosen_indices();
let (left_title, right_title) = self.titles.clone();
let left_pane = self.pane(&left_title, left, "guise-transfer-left", cx);
let right_pane = self.pane(&right_title, right, "guise-transfer-right", cx);
let mut buttons = div().flex().flex_col().justify_center().gap(px(6.0));
for (key, icon, to_right) in [
("guise-transfer-toright", IconName::ChevronRight, true),
("guise-transfer-toleft", IconName::ChevronLeft, false),
] {
buttons = buttons.child(
div()
.id(key)
.flex()
.items_center()
.justify_center()
.w(px(26.0))
.h(px(26.0))
.rounded(px(6.0))
.bg(surface)
.border_1()
.border_color(border)
.text_color(dimmed)
.hover(move |s| s.bg(surface_hover))
.child(Icon::new(icon).size(Size::Xs))
.on_click(cx.listener(move |this, _ev, _window, cx| {
if !this.disabled {
this.transfer(to_right, cx);
}
})),
);
}
let row = div()
.track_focus(&self.focus)
.flex()
.items_center()
.gap(px(10.0))
.w_full()
.child(left_pane)
.child(buttons)
.child(right_pane);
if self.disabled {
row.opacity(0.6)
} else {
row
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn moves_only_checked_items_on_the_source_side() {
let mut right: BTreeSet<usize> = [3].into_iter().collect();
let mut checked: BTreeSet<usize> = [0, 1, 3].into_iter().collect();
assert!(move_checked(&mut right, &mut checked, true));
assert_eq!(right.iter().copied().collect::<Vec<_>>(), vec![0, 1, 3]);
assert!(checked.contains(&3) && !checked.contains(&0));
assert!(move_checked(&mut right, &mut checked, false));
assert_eq!(right.iter().copied().collect::<Vec<_>>(), vec![0, 1]);
assert!(checked.is_empty());
}
#[test]
fn no_movers_reports_false() {
let mut right = BTreeSet::new();
let mut checked = BTreeSet::new();
assert!(!move_checked(&mut right, &mut checked, true));
checked.insert(2);
assert!(!move_checked(&mut right, &mut checked, false));
assert!(checked.contains(&2));
}
}