split_button.rs

  1use gpui::{
  2    AnyElement, App, BoxShadow, IntoElement, ParentElement, RenderOnce, Styled, Window, div, hsla,
  3    point, prelude::FluentBuilder, px, relative,
  4};
  5use theme::ActiveTheme;
  6
  7use crate::{ElevationIndex, prelude::*};
  8
  9use super::ButtonLike;
 10
 11#[derive(Clone, Copy, PartialEq)]
 12pub enum SplitButtonStyle {
 13    Filled,
 14    Outlined,
 15    Transparent,
 16}
 17
 18pub enum SplitButtonKind {
 19    ButtonLike(ButtonLike),
 20    IconButton(IconButton),
 21}
 22
 23impl From<IconButton> for SplitButtonKind {
 24    fn from(icon_button: IconButton) -> Self {
 25        Self::IconButton(icon_button)
 26    }
 27}
 28
 29impl From<ButtonLike> for SplitButtonKind {
 30    fn from(button_like: ButtonLike) -> Self {
 31        Self::ButtonLike(button_like)
 32    }
 33}
 34
 35/// /// A button with two parts: a primary action on the left and a secondary action on the right.
 36///
 37/// The left side is a [`ButtonLike`] with the main action, while the right side can contain
 38/// any element (typically a dropdown trigger or similar).
 39///
 40/// The two sections are visually separated by a divider, but presented as a unified control.
 41#[derive(IntoElement)]
 42pub struct SplitButton {
 43    left: SplitButtonKind,
 44    right: AnyElement,
 45    style: SplitButtonStyle,
 46}
 47
 48impl SplitButton {
 49    pub fn new(left: impl Into<SplitButtonKind>, right: AnyElement) -> Self {
 50        Self {
 51            left: left.into(),
 52            right,
 53            style: SplitButtonStyle::Filled,
 54        }
 55    }
 56
 57    pub fn style(mut self, style: SplitButtonStyle) -> Self {
 58        self.style = style;
 59        self
 60    }
 61}
 62
 63impl RenderOnce for SplitButton {
 64    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
 65        let is_filled_or_outlined = matches!(
 66            self.style,
 67            SplitButtonStyle::Filled | SplitButtonStyle::Outlined
 68        );
 69
 70        h_flex()
 71            .when(is_filled_or_outlined, |this| {
 72                this.rounded_sm()
 73                    .border_1()
 74                    .border_color(cx.theme().colors().border.opacity(0.8))
 75            })
 76            .when(self.style == SplitButtonStyle::Transparent, |this| {
 77                this.gap_px()
 78            })
 79            .child(div().flex_grow().child(match self.left {
 80                SplitButtonKind::ButtonLike(button) => button.into_any_element(),
 81                SplitButtonKind::IconButton(icon) => icon.into_any_element(),
 82            }))
 83            .child(
 84                div()
 85                    .h(relative(0.8))
 86                    .w_px()
 87                    .bg(cx.theme().colors().border.opacity(0.5)),
 88            )
 89            .child(self.right)
 90            .when(self.style == SplitButtonStyle::Filled, |this| {
 91                this.bg(ElevationIndex::Surface.on_elevation_bg(cx))
 92                    .shadow(vec![BoxShadow {
 93                        color: hsla(0.0, 0.0, 0.0, 0.16),
 94                        offset: point(px(0.), px(1.)),
 95                        blur_radius: px(0.),
 96                        spread_radius: px(0.),
 97                    }])
 98            })
 99    }
100}