T05 buttons and actions: groups, toggles, segmented, confirm, copy, share, bars, sheet, quick actions
This commit is contained in:
29 files changed
+2659
-151
No files matched your search
@@ -0,0 +1,311 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
App, ElementId, FontWeight, InteractiveElement, IntoElement, MouseButton, ParentElement,
|
||||
RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, div, prelude::*,
|
||||
};
|
||||
|
||||
use super::{
|
||||
Button,
|
||||
button::{Slot, label_size},
|
||||
};
|
||||
use crate::{
|
||||
primitives::{FocusRing, Icon, IconName, Tooltip},
|
||||
theme::{ActiveTheme, ControlSize, Radius},
|
||||
};
|
||||
|
||||
/// Buttons joined edge to edge.
|
||||
#[derive(IntoElement, Default)]
|
||||
pub struct ButtonGroup {
|
||||
buttons: Vec<Button>,
|
||||
}
|
||||
|
||||
impl ButtonGroup {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn button(mut self, button: Button) -> Self {
|
||||
self.buttons.push(button);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ButtonGroup {
|
||||
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
|
||||
let count = self.buttons.len();
|
||||
assert!(count >= 2, "a button group joins two buttons or more");
|
||||
div().flex().children(
|
||||
self.buttons
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(ix, mut button)| {
|
||||
button.slot = Some(match ix {
|
||||
0 => Slot::First,
|
||||
ix if ix + 1 == count => Slot::Last,
|
||||
_ => Slot::Middle,
|
||||
});
|
||||
button
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// One choice in a toggle: a label, an icon, or both.
|
||||
#[derive(Clone)]
|
||||
pub struct ToggleItem {
|
||||
value: SharedString,
|
||||
label: Option<SharedString>,
|
||||
icon: Option<IconName>,
|
||||
tooltip: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl ToggleItem {
|
||||
pub fn new(value: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
value: value.into(),
|
||||
label: None,
|
||||
icon: None,
|
||||
tooltip: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
|
||||
self.label = Some(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn icon(mut self, icon: IconName) -> Self {
|
||||
self.icon = Some(icon);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn tooltip(mut self, text: impl Into<SharedString>) -> Self {
|
||||
self.tooltip = Some(text.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
type OnPress = Rc<dyn Fn(&mut Window, &mut App)>;
|
||||
|
||||
fn toggle_face(
|
||||
id: ElementId,
|
||||
item: &ToggleItem,
|
||||
on: bool,
|
||||
size: ControlSize,
|
||||
press: OnPress,
|
||||
cx: &App,
|
||||
) -> impl IntoElement + use<> {
|
||||
assert!(
|
||||
item.label.is_some() || item.icon.is_some(),
|
||||
"toggle {} needs a label or an icon",
|
||||
item.value
|
||||
);
|
||||
let theme = cx.theme();
|
||||
let colors = &theme.colors;
|
||||
let (text, icon_size) = label_size(size);
|
||||
let fg = if on { colors.fg } else { colors.fg_muted };
|
||||
let hover = colors.hover;
|
||||
div()
|
||||
.id(id)
|
||||
.flex()
|
||||
.flex_none()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_1p5()
|
||||
.h(theme.control_height(size))
|
||||
.min_w(theme.control_height(size))
|
||||
.when(item.label.is_some(), |face| {
|
||||
face.px(theme.control_padding(size))
|
||||
})
|
||||
.rounded(theme.radius(Radius::Md))
|
||||
.border_1()
|
||||
.border_color(gpui::transparent_black())
|
||||
.text_size(theme.text_size(text))
|
||||
.font_weight(FontWeight::MEDIUM)
|
||||
.text_color(fg)
|
||||
.map(|face| {
|
||||
if on {
|
||||
face.bg(colors.active)
|
||||
} else {
|
||||
face.hover(|style| style.bg(hover))
|
||||
}
|
||||
})
|
||||
.cursor_pointer()
|
||||
.tab_index(0)
|
||||
.focus_ring(cx)
|
||||
.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
|
||||
.on_click(move |_, window, cx| press(window, cx))
|
||||
.when_some(item.icon, |face, icon| {
|
||||
face.child(Icon::new(icon).size(icon_size).color(fg))
|
||||
})
|
||||
.when_some(item.label.clone(), |face, label| face.child(label))
|
||||
.when_some(item.tooltip.clone(), |face, text| {
|
||||
face.tooltip(Tooltip::text(text))
|
||||
})
|
||||
}
|
||||
|
||||
type OnToggle = Rc<dyn Fn(bool, &mut Window, &mut App)>;
|
||||
|
||||
/// A button that stays pressed until pressed again.
|
||||
#[derive(IntoElement)]
|
||||
pub struct ToggleButton {
|
||||
id: ElementId,
|
||||
item: ToggleItem,
|
||||
on: bool,
|
||||
size: ControlSize,
|
||||
on_toggle: Option<OnToggle>,
|
||||
}
|
||||
|
||||
impl ToggleButton {
|
||||
pub fn new(id: impl Into<ElementId>, item: ToggleItem, on: bool) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
item,
|
||||
on,
|
||||
size: ControlSize::default(),
|
||||
on_toggle: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(mut self, size: ControlSize) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_toggle(mut self, handler: impl Fn(bool, &mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_toggle = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ToggleButton {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let (on, toggle, value) = (self.on, self.on_toggle, self.item.value.clone());
|
||||
let press: OnPress = Rc::new(move |window, cx| {
|
||||
log::info!("toggle {value}: {}", !on);
|
||||
if let Some(toggle) = &toggle {
|
||||
toggle(!on, window, cx);
|
||||
}
|
||||
});
|
||||
toggle_face(self.id, &self.item, on, self.size, press, cx)
|
||||
}
|
||||
}
|
||||
|
||||
type OnChange = Rc<dyn Fn(&[SharedString], &mut Window, &mut App)>;
|
||||
|
||||
/// Toggles side by side. One is chosen, or several with `multiple`.
|
||||
#[derive(IntoElement)]
|
||||
pub struct ToggleGroup {
|
||||
id: ElementId,
|
||||
items: Vec<ToggleItem>,
|
||||
selected: Vec<SharedString>,
|
||||
multiple: bool,
|
||||
size: ControlSize,
|
||||
on_change: Option<OnChange>,
|
||||
}
|
||||
|
||||
impl ToggleGroup {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
items: Vec::new(),
|
||||
selected: Vec::new(),
|
||||
multiple: false,
|
||||
size: ControlSize::default(),
|
||||
on_change: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn item(mut self, item: ToggleItem) -> Self {
|
||||
self.items.push(item);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn selected(mut self, values: impl IntoIterator<Item = impl Into<SharedString>>) -> Self {
|
||||
self.selected = values.into_iter().map(Into::into).collect();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn multiple(mut self) -> Self {
|
||||
self.multiple = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn size(mut self, size: ControlSize) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Reports the whole selection after each press.
|
||||
pub fn on_change(
|
||||
mut self,
|
||||
handler: impl Fn(&[SharedString], &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_change = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// The selection after pressing `value`.
|
||||
fn pressed(selected: &[SharedString], value: &SharedString, multiple: bool) -> Vec<SharedString> {
|
||||
let on = selected.contains(value);
|
||||
match (multiple, on) {
|
||||
(_, true) => selected.iter().filter(|v| *v != value).cloned().collect(),
|
||||
(true, false) => selected.iter().chain([value]).cloned().collect(),
|
||||
(false, false) => vec![value.clone()],
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ToggleGroup {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let faces: Vec<_> = self
|
||||
.items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, item)| {
|
||||
let on = self.selected.contains(&item.value);
|
||||
let next = pressed(&self.selected, &item.value, self.multiple);
|
||||
let change = self.on_change.clone();
|
||||
let press: OnPress = Rc::new(move |window, cx| {
|
||||
log::info!("toggle group: {next:?}");
|
||||
if let Some(change) = &change {
|
||||
change(&next, window, cx);
|
||||
}
|
||||
});
|
||||
toggle_face(("toggle", ix).into(), item, on, self.size, press, cx)
|
||||
})
|
||||
.collect();
|
||||
div()
|
||||
.id(self.id)
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_0p5()
|
||||
.p_0p5()
|
||||
.rounded(theme.radius(Radius::Lg))
|
||||
.border_1()
|
||||
.border_color(theme.colors.border)
|
||||
.children(faces)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::SharedString;
|
||||
|
||||
use super::pressed;
|
||||
|
||||
fn values(list: &[&'static str]) -> Vec<SharedString> {
|
||||
list.iter().map(|v| SharedString::from(*v)).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_replaces_multiple_adds_and_pressing_again_clears() {
|
||||
let b = SharedString::from("b");
|
||||
assert_eq!(pressed(&values(&["a"]), &b, false), values(&["b"]));
|
||||
assert_eq!(pressed(&values(&["a"]), &b, true), values(&["a", "b"]));
|
||||
assert_eq!(pressed(&values(&["a", "b"]), &b, true), values(&["a"]));
|
||||
assert_eq!(pressed(&values(&["b"]), &b, false), values(&[]));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user