app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

primitives.rs (48752B)


      1 use gpui::{
      2     AnyElement, App, ClickEvent, Context, Corner, Div, ElementId, Entity, InteractiveElement,
      3     IntoElement, ParentElement, SharedString, StatefulInteractiveElement, Styled, Window, div,
      4     prelude::FluentBuilder, px, relative, rgb, transparent_black,
      5 };
      6 use gpui_component::{
      7     Icon, IconName, Sizable, Size,
      8     button::{Button, ButtonCustomVariant, ButtonRounded, ButtonVariants, DropdownButton},
      9     input::{Input, InputState},
     10     menu::{DropdownMenu, PopupMenu},
     11     tab::{Tab, TabBar},
     12 };
     13 use std::rc::Rc;
     14 
     15 use crate::APP_UI_THEME;
     16 
     17 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
     18 enum AppButtonVariant {
     19     Secondary,
     20     Primary,
     21 }
     22 
     23 pub struct AppSegmentButtonIconSpec {
     24     pub id: &'static str,
     25     pub label: SharedString,
     26     pub icon: IconName,
     27 }
     28 
     29 impl AppSegmentButtonIconSpec {
     30     pub fn new(id: &'static str, label: impl Into<SharedString>, icon: IconName) -> Self {
     31         Self {
     32             id,
     33             label: label.into(),
     34             icon,
     35         }
     36     }
     37 }
     38 
     39 pub struct AppIconButtonSpec {
     40     pub id: &'static str,
     41     pub label: SharedString,
     42     pub icon: IconName,
     43 }
     44 
     45 impl AppIconButtonSpec {
     46     pub fn new(id: &'static str, label: impl Into<SharedString>, icon: IconName) -> Self {
     47         Self {
     48             id,
     49             label: label.into(),
     50             icon,
     51         }
     52     }
     53 }
     54 
     55 pub struct AppCheckboxFieldSpec {
     56     pub id: &'static str,
     57     pub label: SharedString,
     58     pub note: Option<SharedString>,
     59 }
     60 
     61 impl AppCheckboxFieldSpec {
     62     pub fn new(
     63         id: &'static str,
     64         label: impl Into<SharedString>,
     65         note: Option<impl Into<SharedString>>,
     66     ) -> Self {
     67         Self {
     68             id,
     69             label: label.into(),
     70             note: note.map(Into::into),
     71         }
     72     }
     73 }
     74 
     75 pub struct AppFormFieldSpec {
     76     pub label: SharedString,
     77     pub note: Option<SharedString>,
     78 }
     79 
     80 impl AppFormFieldSpec {
     81     pub fn new(label: impl Into<SharedString>, note: Option<impl Into<SharedString>>) -> Self {
     82         Self {
     83             label: label.into(),
     84             note: note.map(Into::into),
     85         }
     86     }
     87 }
     88 
     89 pub struct AppUnderlineTabSpec {
     90     pub label: SharedString,
     91 }
     92 
     93 impl AppUnderlineTabSpec {
     94     pub fn new(label: impl Into<SharedString>) -> Self {
     95         Self {
     96             label: label.into(),
     97         }
     98     }
     99 }
    100 
    101 pub struct AppPillTabSpec {
    102     pub label: SharedString,
    103 }
    104 
    105 impl AppPillTabSpec {
    106     pub fn new(label: impl Into<SharedString>) -> Self {
    107         Self {
    108             label: label.into(),
    109         }
    110     }
    111 }
    112 
    113 #[derive(Clone, Debug, Eq, PartialEq)]
    114 pub struct LabelValueRow {
    115     pub label: SharedString,
    116     pub value: SharedString,
    117 }
    118 
    119 impl LabelValueRow {
    120     pub fn new(label: impl Into<SharedString>, value: impl Into<SharedString>) -> Self {
    121         Self {
    122             label: label.into(),
    123             value: value.into(),
    124         }
    125     }
    126 }
    127 
    128 pub fn app_surface_window(background: u32, content: impl IntoElement) -> impl IntoElement {
    129     div()
    130         .size_full()
    131         .overflow_hidden()
    132         .bg(rgb(background))
    133         .text_color(rgb(APP_UI_THEME.foundation.text.primary))
    134         .child(content)
    135 }
    136 
    137 pub fn app_surface_sidebar(content: impl IntoElement) -> impl IntoElement {
    138     div()
    139         .h_full()
    140         .bg(rgb(APP_UI_THEME.foundation.surfaces.card_background))
    141         .child(content)
    142 }
    143 
    144 pub fn app_surface_panel(content: impl IntoElement) -> impl IntoElement {
    145     div()
    146         .w_full()
    147         .bg(rgb(APP_UI_THEME.foundation.surfaces.chrome_background))
    148         .rounded(px(APP_UI_THEME.foundation.radii.medium_px))
    149         .child(content)
    150 }
    151 
    152 pub fn app_surface_card(content: impl IntoElement) -> impl IntoElement {
    153     div()
    154         .w_full()
    155         .bg(rgb(APP_UI_THEME.foundation.surfaces.card_background))
    156         .rounded(px(APP_UI_THEME.foundation.radii.medium_px))
    157         .child(
    158             div()
    159                 .w_full()
    160                 .p(px(APP_UI_THEME.shells.home_card_padding_px))
    161                 .child(content),
    162         )
    163 }
    164 
    165 pub fn app_surface_card_section(
    166     title: impl Into<SharedString>,
    167     body: impl IntoElement,
    168 ) -> impl IntoElement {
    169     app_surface_card(app_form_section(title, body))
    170 }
    171 
    172 pub fn app_focused_task_view(
    173     title: impl Into<SharedString>,
    174     body: impl IntoElement,
    175     actions: impl IntoElement,
    176 ) -> AnyElement {
    177     app_focused_view(
    178         APP_UI_THEME.shells.focused_task_max_width_px,
    179         title,
    180         body,
    181         actions,
    182     )
    183 }
    184 
    185 pub fn app_focused_detail_view(
    186     title: impl Into<SharedString>,
    187     body: impl IntoElement,
    188     actions: impl IntoElement,
    189 ) -> AnyElement {
    190     app_focused_view(
    191         APP_UI_THEME.shells.focused_detail_max_width_px,
    192         title,
    193         body,
    194         actions,
    195     )
    196 }
    197 
    198 fn app_focused_view(
    199     max_width_px: f32,
    200     title: impl Into<SharedString>,
    201     body: impl IntoElement,
    202     actions: impl IntoElement,
    203 ) -> AnyElement {
    204     div()
    205         .w_full()
    206         .max_w(px(max_width_px))
    207         .mx_auto()
    208         .child(app_surface_card(
    209             app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
    210                 .w_full()
    211                 .child(
    212                     div()
    213                         .w_full()
    214                         .flex()
    215                         .items_start()
    216                         .justify_between()
    217                         .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
    218                         .child(app_text_value(title))
    219                         .child(actions),
    220                 )
    221                 .child(body),
    222         ))
    223         .into_any_element()
    224 }
    225 
    226 pub fn app_stack_v(gap_px: f32) -> Div {
    227     div().flex().flex_col().gap(px(gap_px))
    228 }
    229 
    230 pub fn app_stack_h(gap_px: f32) -> Div {
    231     div().flex().items_center().gap(px(gap_px))
    232 }
    233 
    234 pub fn app_cluster(gap_px: f32) -> Div {
    235     div().flex().flex_wrap().items_center().gap(px(gap_px))
    236 }
    237 
    238 pub fn app_split_shell(sidebar: impl IntoElement, main_content: impl IntoElement) -> AnyElement {
    239     let sidebar = sidebar.into_any_element();
    240     let main_content = main_content.into_any_element();
    241 
    242     app_surface_window(
    243         APP_UI_THEME.foundation.surfaces.window_background,
    244         div()
    245             .size_full()
    246             .overflow_hidden()
    247             .flex()
    248             .child(sidebar)
    249             .child(
    250                 div()
    251                     .h_full()
    252                     .w(px(APP_UI_THEME.foundation.borders.divider_thickness_px))
    253                     .bg(rgb(APP_UI_THEME.foundation.surfaces.divider)),
    254             )
    255             .child(
    256                 div()
    257                     .flex_1()
    258                     .h_full()
    259                     .bg(rgb(APP_UI_THEME.foundation.surfaces.window_background))
    260                     .overflow_hidden()
    261                     .child(
    262                         div()
    263                             .size_full()
    264                             .p(px(APP_UI_THEME.shells.home_window_padding_px))
    265                             .child(main_content),
    266                     ),
    267             ),
    268     )
    269     .into_any_element()
    270 }
    271 
    272 pub fn app_scroll_panel(
    273     id: &'static str,
    274     content_padding_px: f32,
    275     content_max_width_px: Option<f32>,
    276     content: impl IntoElement,
    277 ) -> AnyElement {
    278     let content = content.into_any_element();
    279     let content: AnyElement = match content_max_width_px {
    280         Some(content_max_width_px) => div()
    281             .w_full()
    282             .max_w(px(content_max_width_px))
    283             .mx_auto()
    284             .child(content)
    285             .into_any_element(),
    286         None => div().w_full().child(content).into_any_element(),
    287     };
    288 
    289     div()
    290         .id(id)
    291         .size_full()
    292         .overflow_y_scroll()
    293         .child(
    294             div()
    295                 .w_full()
    296                 .when(content_padding_px > 0.0, |this| {
    297                     this.p(px(content_padding_px))
    298                 })
    299                 .child(content),
    300         )
    301         .into_any_element()
    302 }
    303 
    304 pub fn app_divider() -> impl IntoElement {
    305     div()
    306         .w_full()
    307         .h(px(APP_UI_THEME.foundation.borders.divider_thickness_px))
    308         .bg(rgb(APP_UI_THEME.foundation.surfaces.divider))
    309 }
    310 
    311 pub fn app_underline_tabs(
    312     id: &'static str,
    313     tabs: impl IntoIterator<Item = AppUnderlineTabSpec>,
    314     selected_index: usize,
    315     on_click: impl Fn(&usize, &mut Window, &mut App) + 'static,
    316 ) -> impl IntoElement {
    317     let tab_text_px = APP_UI_THEME.foundation.typography.body_text_px + 1.0;
    318     let active_foreground = APP_UI_THEME.components.app_button.primary_colors.background;
    319     let inactive_foreground = APP_UI_THEME.foundation.text.secondary;
    320     let tab_gap_px = 16.0;
    321     let tabs = tabs.into_iter().collect::<Vec<_>>();
    322     let tab_widths = tabs
    323         .iter()
    324         .map(|tab| app_underline_tab_width_px(&tab.label, tab_text_px))
    325         .collect::<Vec<_>>();
    326     let selected_width_px = tab_widths.get(selected_index).copied().unwrap_or(0.0);
    327     let selected_offset_px = tab_widths.iter().take(selected_index).copied().sum::<f32>()
    328         + tab_gap_px * selected_index as f32;
    329 
    330     div()
    331         .relative()
    332         .w_full()
    333         .child(
    334             TabBar::new(id)
    335                 .underline()
    336                 .with_size(Size::Medium)
    337                 .w_full()
    338                 .children(
    339                     tabs.into_iter()
    340                         .enumerate()
    341                         .map(|(index, tab)| {
    342                             let is_selected = index == selected_index;
    343                             let foreground = if is_selected {
    344                                 active_foreground
    345                             } else {
    346                                 inactive_foreground
    347                             };
    348 
    349                             Tab::new().child(
    350                                 div()
    351                                     .w(px(tab_widths.get(index).copied().unwrap_or(36.0)))
    352                                     .text_size(px(tab_text_px))
    353                                     .font_weight(gpui::FontWeight::MEDIUM)
    354                                     .text_color(rgb(foreground))
    355                                     .child(tab.label),
    356                             )
    357                         })
    358                         .collect::<Vec<_>>(),
    359                 )
    360                 .on_click(on_click),
    361         )
    362         .child(
    363             div()
    364                 .absolute()
    365                 .left(px(selected_offset_px))
    366                 .bottom_0()
    367                 .w(px(selected_width_px))
    368                 .h(px(2.0))
    369                 .rounded(px(1.0))
    370                 .bg(rgb(active_foreground)),
    371         )
    372 }
    373 
    374 pub fn app_pill_tabs(
    375     id: &'static str,
    376     tabs: impl IntoIterator<Item = AppPillTabSpec>,
    377     selected_index: usize,
    378     on_click: impl Fn(&usize, &mut Window, &mut App) + 'static,
    379     cx: &App,
    380 ) -> impl IntoElement {
    381     let on_click: Rc<dyn Fn(&usize, &mut Window, &mut App)> = Rc::new(on_click);
    382     let tabs = tabs.into_iter().collect::<Vec<_>>();
    383     let height_px = APP_UI_THEME.components.app_button.sizing.height_px;
    384     let radius_px = height_px / 2.0;
    385     let horizontal_padding_px = APP_UI_THEME
    386         .components
    387         .app_button
    388         .sizing
    389         .compact_horizontal_padding_px;
    390     let label_size_px = APP_UI_THEME.components.app_button.sizing.label_size_px;
    391     let gap_px = APP_UI_THEME.foundation.spacing.micro_px;
    392     let primary = APP_UI_THEME.components.app_button.primary_colors;
    393     let inactive_foreground = APP_UI_THEME.foundation.text.secondary;
    394     let inactive_hover_background = APP_UI_THEME.foundation.surfaces.card_background;
    395 
    396     div()
    397         .id(id)
    398         .flex()
    399         .items_center()
    400         .w_full()
    401         .gap(px(gap_px))
    402         .children(tabs.into_iter().enumerate().map(|(index, tab)| {
    403             let is_selected = index == selected_index;
    404             let foreground = if is_selected {
    405                 primary.foreground
    406             } else {
    407                 inactive_foreground
    408             };
    409             let variant = if is_selected {
    410                 ButtonCustomVariant::new(cx)
    411                     .color(rgb(primary.background).into())
    412                     .foreground(rgb(primary.foreground).into())
    413                     .border(transparent_black())
    414                     .hover(rgb(primary.hover_background).into())
    415                     .active(rgb(primary.active_background).into())
    416             } else {
    417                 ButtonCustomVariant::new(cx)
    418                     .color(transparent_black().into())
    419                     .foreground(rgb(inactive_foreground).into())
    420                     .border(transparent_black())
    421                     .hover(rgb(inactive_hover_background).into())
    422                     .active(rgb(inactive_hover_background).into())
    423             };
    424             let on_click = Rc::clone(&on_click);
    425 
    426             Button::new((id, index))
    427                 .custom(variant)
    428                 .h(px(height_px))
    429                 .rounded(ButtonRounded::Size(px(radius_px)))
    430                 .on_click(move |_, window, cx| on_click(&index, window, cx))
    431                 .child(
    432                     div()
    433                         .h_full()
    434                         .flex()
    435                         .items_center()
    436                         .justify_center()
    437                         .px(px(horizontal_padding_px))
    438                         .text_size(px(label_size_px))
    439                         .font_weight(gpui::FontWeight::MEDIUM)
    440                         .text_color(rgb(foreground))
    441                         .child(tab.label),
    442                 )
    443         }))
    444 }
    445 
    446 fn app_underline_tab_width_px(label: &SharedString, tab_text_px: f32) -> f32 {
    447     (label.as_ref().chars().count() as f32 * tab_text_px * 0.56).max(36.0)
    448 }
    449 
    450 pub fn app_heading_view(content: impl Into<SharedString>) -> impl IntoElement {
    451     div()
    452         .w_full()
    453         .text_size(px(APP_UI_THEME.foundation.typography.startup_title_text_px))
    454         .font_weight(gpui::FontWeight::NORMAL)
    455         .text_color(rgb(APP_UI_THEME.foundation.text.primary))
    456         .child(content.into())
    457 }
    458 
    459 pub fn app_heading_section(content: impl Into<SharedString>) -> impl IntoElement {
    460     div()
    461         .w_full()
    462         .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
    463         .font_weight(gpui::FontWeight::SEMIBOLD)
    464         .text_color(rgb(APP_UI_THEME.foundation.text.primary))
    465         .child(content.into())
    466 }
    467 
    468 pub fn app_text_body(content: impl Into<SharedString>) -> impl IntoElement {
    469     div()
    470         .w_full()
    471         .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
    472         .line_height(relative(1.2))
    473         .text_color(rgb(APP_UI_THEME.foundation.text.primary))
    474         .child(content.into())
    475 }
    476 
    477 pub fn app_text_body_subtle(content: impl Into<SharedString>) -> impl IntoElement {
    478     div()
    479         .w_full()
    480         .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
    481         .line_height(relative(1.2))
    482         .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
    483         .child(content.into())
    484 }
    485 
    486 pub fn app_text_label(content: impl Into<SharedString>) -> impl IntoElement {
    487     div()
    488         .w_full()
    489         .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
    490         .font_weight(gpui::FontWeight::MEDIUM)
    491         .text_color(rgb(APP_UI_THEME.foundation.text.primary))
    492         .child(content.into())
    493 }
    494 
    495 pub fn app_text_value(content: impl Into<SharedString>) -> impl IntoElement {
    496     div()
    497         .w_full()
    498         .text_size(px(APP_UI_THEME.foundation.typography.body_text_px * 2.0))
    499         .font_weight(gpui::FontWeight::BOLD)
    500         .text_color(rgb(APP_UI_THEME.foundation.text.primary))
    501         .child(content.into())
    502 }
    503 
    504 pub fn app_text_badge(content: impl Into<SharedString>) -> impl IntoElement {
    505     div()
    506         .w_full()
    507         .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
    508         .font_weight(gpui::FontWeight::SEMIBOLD)
    509         .text_color(rgb(APP_UI_THEME.foundation.text.accent))
    510         .child(content.into())
    511 }
    512 
    513 pub fn utility_title_row(title: impl Into<SharedString>) -> impl IntoElement {
    514     div()
    515         .w_full()
    516         .h(px(APP_UI_THEME.shells.utility_title_row_height_px))
    517         .flex()
    518         .justify_center()
    519         .items_center()
    520         .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
    521         .font_weight(gpui::FontWeight::BOLD)
    522         .text_color(rgb(APP_UI_THEME.foundation.text.primary))
    523         .child(title.into())
    524 }
    525 
    526 pub fn label_value_list(rows: impl IntoIterator<Item = LabelValueRow>) -> impl IntoElement {
    527     let rows = rows
    528         .into_iter()
    529         .map(|row| {
    530             let line = format!("{}: {}", row.label, row.value);
    531             div()
    532                 .w_full()
    533                 .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
    534                 .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
    535                 .child(line)
    536         })
    537         .collect::<Vec<_>>();
    538 
    539     div()
    540         .w_full()
    541         .flex()
    542         .flex_col()
    543         .gap(px(APP_UI_THEME.shells.metadata_row_gap_px))
    544         .children(rows)
    545 }
    546 
    547 pub fn app_detail_row(label: impl Into<SharedString>, value: impl IntoElement) -> impl IntoElement {
    548     div()
    549         .w_full()
    550         .flex()
    551         .items_center()
    552         .gap(px(APP_UI_THEME.shells.settings_account_detail_value_gap_px))
    553         .child(
    554             div()
    555                 .text_size(px(APP_UI_THEME
    556                     .foundation
    557                     .typography
    558                     .settings_account_detail_text_px))
    559                 .font_weight(gpui::FontWeight::SEMIBOLD)
    560                 .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
    561                 .child(label.into()),
    562         )
    563         .child(value)
    564 }
    565 
    566 pub fn app_form_section(
    567     title: impl Into<SharedString>,
    568     content: impl IntoElement,
    569 ) -> impl IntoElement {
    570     div()
    571         .w_full()
    572         .flex()
    573         .flex_col()
    574         .items_start()
    575         .gap(px(APP_UI_THEME.foundation.spacing.small_px))
    576         .child(app_heading_section(title))
    577         .child(content)
    578 }
    579 
    580 pub fn app_form_field(spec: AppFormFieldSpec, field: impl IntoElement) -> impl IntoElement {
    581     div()
    582         .w_full()
    583         .flex()
    584         .flex_col()
    585         .items_start()
    586         .gap(px(APP_UI_THEME.foundation.spacing.tight_px))
    587         .child(app_text_label(spec.label))
    588         .child(field)
    589         .when_some(spec.note, |this, note| {
    590             this.child(app_text_body_subtle(note))
    591         })
    592 }
    593 
    594 pub fn app_form_input_text(
    595     spec: AppFormFieldSpec,
    596     input: &Entity<InputState>,
    597     disabled: bool,
    598 ) -> impl IntoElement {
    599     app_form_field(spec, app_input_text(input, disabled).w_full())
    600 }
    601 
    602 fn app_checkbox(
    603     id: &'static str,
    604     checked: bool,
    605     cx: &App,
    606     on_change: impl Fn(bool, &mut Window, &mut App) + 'static,
    607 ) -> impl IntoElement {
    608     let colors = APP_UI_THEME.components.app_checkbox_field;
    609     let background = if checked {
    610         colors.checked_background
    611     } else {
    612         colors.unchecked_background
    613     };
    614     let border = if checked {
    615         colors.checked_background
    616     } else {
    617         colors.unchecked_border
    618     };
    619     let mut button = Button::new(id)
    620         .custom(
    621             ButtonCustomVariant::new(cx)
    622                 .color(rgb(background).into())
    623                 .foreground(rgb(colors.check_foreground).into())
    624                 .border(rgb(border).into())
    625                 .hover(rgb(background).into())
    626                 .active(rgb(background).into()),
    627         )
    628         .rounded(ButtonRounded::Size(px(colors.corner_radius_px)))
    629         .with_size(Size::Size(px(colors.size_px)))
    630         .on_click(move |_, window, cx| on_change(!checked, window, cx));
    631 
    632     if checked {
    633         button = button.icon(
    634             Icon::new(IconName::Check)
    635                 .with_size(Size::Size(px(colors.icon_size_px)))
    636                 .text_color(rgb(colors.check_foreground)),
    637         );
    638     }
    639 
    640     button.tab_stop(false)
    641 }
    642 
    643 pub fn app_checkbox_button(
    644     id: &'static str,
    645     checked: bool,
    646     cx: &App,
    647     on_change: impl Fn(bool, &mut Window, &mut App) + 'static,
    648 ) -> impl IntoElement {
    649     app_checkbox(id, checked, cx, on_change)
    650 }
    651 
    652 pub fn app_checkbox_field(
    653     spec: AppCheckboxFieldSpec,
    654     checked: bool,
    655     cx: &App,
    656     on_change: impl Fn(bool, &mut Window, &mut App) + 'static,
    657 ) -> impl IntoElement {
    658     let checkbox_id = spec.id;
    659     let checkbox_label = spec.label;
    660     let checkbox_note = spec.note;
    661     let row_text_px = APP_UI_THEME.foundation.typography.settings_row_text_px;
    662     let note_text_px = APP_UI_THEME.foundation.typography.utility_title_text_px;
    663     let note_indent_px = APP_UI_THEME.components.app_checkbox_field.size_px
    664         + APP_UI_THEME.shells.settings_checkbox_label_gap_px;
    665     let on_change = Rc::new(on_change);
    666 
    667     div()
    668         .w_full()
    669         .flex()
    670         .flex_col()
    671         .gap(px(APP_UI_THEME.foundation.spacing.micro_px))
    672         .child(
    673             Button::new((checkbox_id, 0usize))
    674                 .custom(
    675                     ButtonCustomVariant::new(cx)
    676                         .color(transparent_black().into())
    677                         .foreground(rgb(APP_UI_THEME.foundation.text.primary).into())
    678                         .border(transparent_black())
    679                         .hover(transparent_black().into())
    680                         .active(transparent_black().into()),
    681                 )
    682                 .rounded(ButtonRounded::Size(px(0.0)))
    683                 .w_full()
    684                 .p(px(0.0))
    685                 .on_click({
    686                     let on_change = Rc::clone(&on_change);
    687                     move |_, window, cx| on_change(!checked, window, cx)
    688                 })
    689                 .child(
    690                     div()
    691                         .w_full()
    692                         .flex()
    693                         .items_start()
    694                         .gap(px(APP_UI_THEME.shells.settings_checkbox_label_gap_px))
    695                         .child(app_checkbox(checkbox_id, checked, cx, {
    696                             let on_change = Rc::clone(&on_change);
    697                             move |checked, window, cx| on_change(checked, window, cx)
    698                         }))
    699                         .child(
    700                             div()
    701                                 .min_w_0()
    702                                 .text_size(px(row_text_px))
    703                                 .line_height(relative(1.1))
    704                                 .text_color(rgb(APP_UI_THEME.foundation.text.primary))
    705                                 .child(checkbox_label),
    706                         ),
    707                 ),
    708         )
    709         .when_some(checkbox_note, |this, note| {
    710             this.child(
    711                 div()
    712                     .w_full()
    713                     .pl(px(note_indent_px))
    714                     .min_w_0()
    715                     .text_size(px(note_text_px))
    716                     .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
    717                     .child(note),
    718             )
    719         })
    720 }
    721 
    722 pub fn app_segment_button_icon(
    723     spec: AppSegmentButtonIconSpec,
    724     is_active: bool,
    725     on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
    726     cx: &App,
    727 ) -> impl IntoElement {
    728     let colors = APP_UI_THEME.components.app_segment_button_icon.colors;
    729     let sizing = APP_UI_THEME.components.app_segment_button_icon.sizing;
    730     let background = if is_active {
    731         colors.active_background
    732     } else {
    733         colors.inactive_background
    734     };
    735     let foreground = if is_active {
    736         colors.active_foreground
    737     } else {
    738         colors.inactive_foreground
    739     };
    740 
    741     Button::new(spec.id)
    742         .custom(
    743             ButtonCustomVariant::new(cx)
    744                 .color(rgb(background).into())
    745                 .foreground(rgb(foreground).into())
    746                 .border(transparent_black())
    747                 .hover(rgb(background).into())
    748                 .active(rgb(background).into()),
    749         )
    750         .rounded(ButtonRounded::Size(px(sizing.corner_radius_px)))
    751         .h(px(sizing.height_px))
    752         .min_w(px(sizing.height_px))
    753         .on_click(on_click)
    754         .child(
    755             div()
    756                 .h_full()
    757                 .flex()
    758                 .flex_col()
    759                 .justify_between()
    760                 .items_center()
    761                 .px(px(sizing.inner_padding_px))
    762                 .py(px(sizing.inner_padding_px))
    763                 .child(
    764                     Icon::new(spec.icon)
    765                         .with_size(Size::Size(px(sizing.icon_size_px)))
    766                         .text_color(rgb(foreground)),
    767                 )
    768                 .child(
    769                     div()
    770                         .text_size(px(sizing.label_size_px))
    771                         .text_color(rgb(foreground))
    772                         .child(spec.label),
    773                 ),
    774         )
    775 }
    776 
    777 pub fn app_input_text(input: &Entity<InputState>, disabled: bool) -> Input {
    778     let tokens = APP_UI_THEME.components.app_input_text;
    779     let background = if disabled {
    780         tokens.disabled_background
    781     } else {
    782         tokens.background
    783     };
    784     let foreground = if disabled {
    785         APP_UI_THEME.foundation.text.secondary
    786     } else {
    787         APP_UI_THEME.foundation.text.primary
    788     };
    789 
    790     Input::new(input)
    791         .with_size(Size::Medium)
    792         .disabled(disabled)
    793         .focus_bordered(true)
    794         .bg(rgb(background))
    795         .text_color(rgb(foreground))
    796         .border_color(rgb(tokens.border))
    797         .border_1()
    798         .rounded(px(tokens.corner_radius_px))
    799 }
    800 
    801 pub fn app_button_secondary(
    802     id: impl Into<ElementId>,
    803     label: impl Into<SharedString>,
    804     on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
    805     cx: &App,
    806 ) -> impl IntoElement {
    807     app_button_label(
    808         app_button_base(id, AppButtonVariant::Secondary, on_click, cx),
    809         label.into(),
    810         APP_UI_THEME
    811             .components
    812             .app_button
    813             .sizing
    814             .horizontal_padding_px,
    815         AppButtonVariant::Secondary,
    816     )
    817 }
    818 
    819 pub fn app_button_secondary_full_width(
    820     id: impl Into<ElementId>,
    821     label: impl Into<SharedString>,
    822     on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
    823     cx: &App,
    824 ) -> impl IntoElement {
    825     app_button_label(
    826         app_button_base(id, AppButtonVariant::Secondary, on_click, cx),
    827         label.into(),
    828         APP_UI_THEME
    829             .components
    830             .app_button
    831             .sizing
    832             .horizontal_padding_px,
    833         AppButtonVariant::Secondary,
    834     )
    835     .w_full()
    836 }
    837 
    838 pub fn app_button_secondary_disabled(
    839     id: impl Into<ElementId>,
    840     label: impl Into<SharedString>,
    841     cx: &App,
    842 ) -> impl IntoElement {
    843     app_button_label(
    844         app_button_base_disabled(id, AppButtonVariant::Secondary, cx),
    845         label.into(),
    846         APP_UI_THEME
    847             .components
    848             .app_button
    849             .sizing
    850             .horizontal_padding_px,
    851         AppButtonVariant::Secondary,
    852     )
    853 }
    854 
    855 pub fn app_button_primary(
    856     id: impl Into<ElementId>,
    857     label: impl Into<SharedString>,
    858     on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
    859     cx: &App,
    860 ) -> impl IntoElement {
    861     app_button_label(
    862         app_button_base(id, AppButtonVariant::Primary, on_click, cx),
    863         label.into(),
    864         APP_UI_THEME
    865             .components
    866             .app_button
    867             .sizing
    868             .horizontal_padding_px,
    869         AppButtonVariant::Primary,
    870     )
    871 }
    872 
    873 pub fn app_button_primary_compact(
    874     id: impl Into<ElementId>,
    875     label: impl Into<SharedString>,
    876     on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
    877     cx: &App,
    878 ) -> impl IntoElement {
    879     app_button_label(
    880         app_button_base(id, AppButtonVariant::Primary, on_click, cx),
    881         label.into(),
    882         APP_UI_THEME
    883             .components
    884             .app_button
    885             .sizing
    886             .compact_horizontal_padding_px,
    887         AppButtonVariant::Primary,
    888     )
    889 }
    890 
    891 pub fn app_button_primary_compact_disabled(
    892     id: impl Into<ElementId>,
    893     label: impl Into<SharedString>,
    894     cx: &App,
    895 ) -> impl IntoElement {
    896     app_button_label(
    897         app_button_base_disabled(id, AppButtonVariant::Primary, cx),
    898         label.into(),
    899         APP_UI_THEME
    900             .components
    901             .app_button
    902             .sizing
    903             .compact_horizontal_padding_px,
    904         AppButtonVariant::Primary,
    905     )
    906 }
    907 
    908 pub fn app_button_primary_full_width(
    909     id: impl Into<ElementId>,
    910     label: impl Into<SharedString>,
    911     on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
    912     cx: &App,
    913 ) -> impl IntoElement {
    914     app_button_label(
    915         app_button_base(id, AppButtonVariant::Primary, on_click, cx),
    916         label.into(),
    917         APP_UI_THEME
    918             .components
    919             .app_button
    920             .sizing
    921             .horizontal_padding_px,
    922         AppButtonVariant::Primary,
    923     )
    924     .w_full()
    925 }
    926 
    927 pub fn app_button_primary_disabled(
    928     id: impl Into<ElementId>,
    929     label: impl Into<SharedString>,
    930     cx: &App,
    931 ) -> impl IntoElement {
    932     app_button_label(
    933         app_button_base_disabled(id, AppButtonVariant::Primary, cx),
    934         label.into(),
    935         APP_UI_THEME
    936             .components
    937             .app_button
    938             .sizing
    939             .horizontal_padding_px,
    940         AppButtonVariant::Primary,
    941     )
    942 }
    943 
    944 pub fn app_button_compact(
    945     id: impl Into<ElementId>,
    946     label: impl Into<SharedString>,
    947     on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
    948     cx: &App,
    949 ) -> impl IntoElement {
    950     app_button_label(
    951         app_button_base(id, AppButtonVariant::Secondary, on_click, cx),
    952         label.into(),
    953         APP_UI_THEME
    954             .components
    955             .app_button
    956             .sizing
    957             .compact_horizontal_padding_px,
    958         AppButtonVariant::Secondary,
    959     )
    960 }
    961 
    962 pub fn app_button_square_dropdown_secondary(
    963     id: &'static str,
    964     menu: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
    965     _cx: &App,
    966 ) -> impl IntoElement {
    967     let sizing = APP_UI_THEME.components.app_button.sizing;
    968     let colors = APP_UI_THEME.components.app_button.secondary_colors;
    969 
    970     div()
    971         .w(px(sizing.square_width_px))
    972         .h(px(sizing.height_px))
    973         .rounded(px(sizing.corner_radius_px))
    974         .bg(rgb(colors.background))
    975         .overflow_hidden()
    976         .child(
    977             DropdownButton::new(id)
    978                 .button(
    979                     Button::new((id, 0usize))
    980                         .tab_stop(false)
    981                         .w(px(0.0))
    982                         .overflow_hidden()
    983                         .ghost()
    984                         .with_size(Size::Size(px(sizing.square_width_px))),
    985                 )
    986                 .dropdown_menu(menu)
    987                 .ghost()
    988                 .rounded(ButtonRounded::Size(px(sizing.corner_radius_px)))
    989                 .with_size(Size::Size(px(sizing.square_width_px))),
    990         )
    991 }
    992 
    993 pub fn app_button_ellipsis_menu(
    994     id: &'static str,
    995     menu: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
    996     _cx: &App,
    997 ) -> impl IntoElement {
    998     let sizing = APP_UI_THEME.components.app_button.sizing;
    999 
   1000     Button::new(id)
   1001         .ghost()
   1002         .rounded(ButtonRounded::Size(px(APP_UI_THEME
   1003             .foundation
   1004             .radii
   1005             .medium_px)))
   1006         .with_size(Size::Size(px(sizing.square_width_px)))
   1007         .tab_stop(false)
   1008         .child(
   1009             Icon::new(IconName::Ellipsis)
   1010                 .with_size(Size::Size(px(sizing.icon_size_px)))
   1011                 .text_color(rgb(APP_UI_THEME.foundation.text.secondary)),
   1012         )
   1013         .dropdown_menu_with_anchor(Corner::BottomRight, menu)
   1014 }
   1015 
   1016 pub fn app_button_sidebar_account_menu(
   1017     id: &'static str,
   1018     label: impl Into<SharedString>,
   1019     menu: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
   1020     cx: &App,
   1021 ) -> impl IntoElement {
   1022     let label = label.into();
   1023     let sizing = APP_UI_THEME.components.app_button.sizing;
   1024     let row = APP_UI_THEME.components.app_account_selector_row;
   1025     let horizontal_padding_px = APP_UI_THEME
   1026         .shells
   1027         .settings_account_sidebar_footer_button_gap_px;
   1028     let icon_label_gap_px = APP_UI_THEME.foundation.spacing.micro_px;
   1029     let icon_size = Size::Size(px(sizing.icon_size_px));
   1030     let width_px = APP_UI_THEME.shells.home_sidebar_width_px
   1031         - (APP_UI_THEME.shells.home_window_padding_px * 2.0);
   1032 
   1033     Button::new(id)
   1034         .custom(
   1035             ButtonCustomVariant::new(cx)
   1036                 .color(rgb(row.inactive_background).into())
   1037                 .foreground(rgb(APP_UI_THEME.foundation.text.secondary).into())
   1038                 .border(transparent_black())
   1039                 .hover(rgb(row.active_background).into())
   1040                 .active(rgb(row.active_background).into()),
   1041         )
   1042         .w(px(width_px))
   1043         .h(px(sizing.height_px))
   1044         .p(px(0.0))
   1045         .rounded(ButtonRounded::Size(px(APP_UI_THEME
   1046             .shells
   1047             .settings_account_sidebar_button_corner_radius_px)))
   1048         .tab_stop(false)
   1049         .child(
   1050             div()
   1051                 .w(px(width_px))
   1052                 .h_full()
   1053                 .px(px(horizontal_padding_px))
   1054                 .flex()
   1055                 .items_center()
   1056                 .justify_between()
   1057                 .gap(px(APP_UI_THEME
   1058                     .shells
   1059                     .settings_account_sidebar_button_gap_px))
   1060                 .child(
   1061                     div()
   1062                         .flex_1()
   1063                         .flex()
   1064                         .items_center()
   1065                         .min_w_0()
   1066                         .gap(px(icon_label_gap_px))
   1067                         .child(
   1068                             div().flex_none().child(
   1069                                 Icon::new(IconName::CircleUser)
   1070                                     .with_size(icon_size)
   1071                                     .text_color(rgb(APP_UI_THEME.foundation.text.secondary)),
   1072                             ),
   1073                         )
   1074                         .child(
   1075                             div()
   1076                                 .min_w_0()
   1077                                 .truncate()
   1078                                 .text_size(px(APP_UI_THEME
   1079                                     .foundation
   1080                                     .typography
   1081                                     .settings_row_text_px))
   1082                                 .font_weight(gpui::FontWeight::MEDIUM)
   1083                                 .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
   1084                                 .child(label),
   1085                         ),
   1086                 )
   1087                 .child(
   1088                     div().flex_none().child(
   1089                         Icon::new(IconName::ChevronsUpDown)
   1090                             .with_size(icon_size)
   1091                             .text_color(rgb(APP_UI_THEME.foundation.text.secondary)),
   1092                     ),
   1093                 ),
   1094         )
   1095         .dropdown_menu_with_anchor(Corner::TopLeft, menu)
   1096 }
   1097 
   1098 fn app_button_label(
   1099     button: Button,
   1100     label: SharedString,
   1101     horizontal_padding_px: f32,
   1102     variant: AppButtonVariant,
   1103 ) -> Button {
   1104     let sizing = APP_UI_THEME.components.app_button.sizing;
   1105     let colors = app_button_colors(variant);
   1106     button.child(
   1107         div()
   1108             .h_full()
   1109             .flex()
   1110             .items_center()
   1111             .justify_center()
   1112             .px(px(horizontal_padding_px))
   1113             .whitespace_nowrap()
   1114             .text_size(px(sizing.label_size_px))
   1115             .text_color(rgb(colors.foreground))
   1116             .child(label),
   1117     )
   1118 }
   1119 
   1120 pub fn app_button_icon(
   1121     spec: AppIconButtonSpec,
   1122     on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
   1123     cx: &App,
   1124 ) -> impl IntoElement {
   1125     let sizing = APP_UI_THEME.components.app_button.sizing;
   1126     let colors = app_button_colors(AppButtonVariant::Secondary);
   1127 
   1128     app_button_base(spec.id, AppButtonVariant::Secondary, on_click, cx)
   1129         .with_size(Size::Size(px(sizing.square_width_px)))
   1130         .tooltip(spec.label)
   1131         .icon(
   1132             Icon::new(spec.icon)
   1133                 .with_size(Size::Size(px(sizing.icon_size_px)))
   1134                 .text_color(rgb(colors.foreground)),
   1135         )
   1136 }
   1137 
   1138 pub fn app_status_indicator(color: u32) -> impl IntoElement {
   1139     let sizing = APP_UI_THEME.components.app_status_indicator;
   1140 
   1141     div()
   1142         .size(px(sizing.size_px))
   1143         .bg(rgb(color))
   1144         .rounded(px(sizing.size_px / 2.0))
   1145 }
   1146 
   1147 pub fn app_button_text(
   1148     id: impl Into<ElementId>,
   1149     label: impl Into<SharedString>,
   1150     on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
   1151     cx: &App,
   1152 ) -> impl IntoElement {
   1153     Button::new(id)
   1154         .custom(
   1155             ButtonCustomVariant::new(cx)
   1156                 .color(transparent_black().into())
   1157                 .foreground(rgb(APP_UI_THEME.foundation.text.secondary).into())
   1158                 .border(transparent_black())
   1159                 .hover(transparent_black().into())
   1160                 .active(transparent_black().into()),
   1161         )
   1162         .rounded(ButtonRounded::Size(px(0.0)))
   1163         .on_click(on_click)
   1164         .child(
   1165             div()
   1166                 .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
   1167                 .font_weight(gpui::FontWeight::MEDIUM)
   1168                 .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
   1169                 .child(label.into()),
   1170         )
   1171 }
   1172 
   1173 pub fn app_button_choice(
   1174     id: impl Into<ElementId>,
   1175     label: impl Into<SharedString>,
   1176     is_active: bool,
   1177     on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
   1178     cx: &App,
   1179 ) -> AnyElement {
   1180     let variant = if is_active {
   1181         AppButtonVariant::Primary
   1182     } else {
   1183         AppButtonVariant::Secondary
   1184     };
   1185 
   1186     app_button_label(
   1187         app_button_base(id, variant, on_click, cx),
   1188         label.into(),
   1189         APP_UI_THEME
   1190             .components
   1191             .app_button
   1192             .sizing
   1193             .compact_horizontal_padding_px,
   1194         variant,
   1195     )
   1196     .into_any_element()
   1197 }
   1198 
   1199 pub fn app_button_list_row(
   1200     id: impl Into<ElementId>,
   1201     title: impl Into<SharedString>,
   1202     subtitle: Option<SharedString>,
   1203     is_selected: bool,
   1204     on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
   1205     cx: &App,
   1206 ) -> impl IntoElement {
   1207     let selected_background = rgb(APP_UI_THEME.foundation.surfaces.window_background);
   1208 
   1209     Button::new(id)
   1210         .custom(
   1211             ButtonCustomVariant::new(cx)
   1212                 .color(if is_selected {
   1213                     selected_background.into()
   1214                 } else {
   1215                     transparent_black().into()
   1216                 })
   1217                 .foreground(rgb(APP_UI_THEME.foundation.text.primary).into())
   1218                 .border(transparent_black())
   1219                 .hover(selected_background.into())
   1220                 .active(selected_background.into()),
   1221         )
   1222         .rounded(ButtonRounded::Size(px(APP_UI_THEME
   1223             .foundation
   1224             .radii
   1225             .medium_px)))
   1226         .flex_1()
   1227         .min_w_0()
   1228         .on_click(on_click)
   1229         .child(
   1230             div()
   1231                 .w_full()
   1232                 .flex()
   1233                 .flex_col()
   1234                 .items_start()
   1235                 .gap(px(4.0))
   1236                 .px(px(8.0))
   1237                 .py(px(6.0))
   1238                 .child(
   1239                     div()
   1240                         .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
   1241                         .font_weight(gpui::FontWeight::MEDIUM)
   1242                         .text_color(rgb(APP_UI_THEME.foundation.text.primary))
   1243                         .child(title.into()),
   1244                 )
   1245                 .when_some(subtitle, |this, subtitle| {
   1246                     this.child(
   1247                         div()
   1248                             .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
   1249                             .line_height(relative(1.2))
   1250                             .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
   1251                             .child(subtitle),
   1252                     )
   1253                 }),
   1254         )
   1255 }
   1256 
   1257 pub fn app_button_account_selector_row(
   1258     id: impl Into<ElementId>,
   1259     title: impl Into<SharedString>,
   1260     subtitle: impl Into<SharedString>,
   1261     is_selected: bool,
   1262     on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
   1263     cx: &App,
   1264 ) -> impl IntoElement {
   1265     let tokens = APP_UI_THEME.components.app_account_selector_row;
   1266     let background = if is_selected {
   1267         tokens.active_background
   1268     } else {
   1269         tokens.inactive_background
   1270     };
   1271 
   1272     Button::new(id)
   1273         .custom(
   1274             ButtonCustomVariant::new(cx)
   1275                 .color(rgb(background).into())
   1276                 .foreground(rgb(APP_UI_THEME.foundation.text.primary).into())
   1277                 .border(transparent_black())
   1278                 .hover(rgb(background).into())
   1279                 .active(rgb(background).into()),
   1280         )
   1281         .rounded(ButtonRounded::Size(px(APP_UI_THEME
   1282             .shells
   1283             .settings_account_sidebar_button_corner_radius_px)))
   1284         .h(px(APP_UI_THEME
   1285             .shells
   1286             .settings_account_sidebar_button_height_px))
   1287         .w_full()
   1288         .min_w_0()
   1289         .p(px(0.0))
   1290         .on_click(on_click)
   1291         .child(
   1292             div()
   1293                 .w_full()
   1294                 .h_full()
   1295                 .min_w_0()
   1296                 .flex()
   1297                 .items_center()
   1298                 .gap(px(APP_UI_THEME
   1299                     .shells
   1300                     .settings_account_sidebar_button_gap_px))
   1301                 .px(px(APP_UI_THEME.foundation.spacing.small_px))
   1302                 .child(
   1303                     div()
   1304                         .size(px(APP_UI_THEME
   1305                             .shells
   1306                             .settings_account_sidebar_avatar_size_px))
   1307                         .rounded_full()
   1308                         .bg(rgb(APP_UI_THEME.foundation.surfaces.divider))
   1309                         .flex_shrink_0(),
   1310                 )
   1311                 .child(
   1312                     div()
   1313                         .min_w_0()
   1314                         .flex()
   1315                         .flex_col()
   1316                         .items_start()
   1317                         .gap(px(APP_UI_THEME
   1318                             .shells
   1319                             .settings_account_identity_text_gap_px))
   1320                         .child(
   1321                             div()
   1322                                 .w_full()
   1323                                 .min_w_0()
   1324                                 .max_w_full()
   1325                                 .overflow_hidden()
   1326                                 .text_ellipsis()
   1327                                 .whitespace_nowrap()
   1328                                 .text_size(px(APP_UI_THEME
   1329                                     .foundation
   1330                                     .typography
   1331                                     .settings_account_identity_text_px
   1332                                     + 1.0))
   1333                                 .font_weight(gpui::FontWeight::LIGHT)
   1334                                 .text_color(rgb(APP_UI_THEME.foundation.text.primary))
   1335                                 .child(title.into()),
   1336                         )
   1337                         .child(
   1338                             div()
   1339                                 .w_full()
   1340                                 .min_w_0()
   1341                                 .max_w_full()
   1342                                 .overflow_hidden()
   1343                                 .text_ellipsis()
   1344                                 .whitespace_nowrap()
   1345                                 .text_size(px(APP_UI_THEME
   1346                                     .foundation
   1347                                     .typography
   1348                                     .utility_title_text_px))
   1349                                 .font_weight(gpui::FontWeight::NORMAL)
   1350                                 .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
   1351                                 .child(subtitle.into()),
   1352                         ),
   1353                 ),
   1354         )
   1355 }
   1356 
   1357 pub fn app_button_card(
   1358     id: impl Into<ElementId>,
   1359     is_selected: bool,
   1360     on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
   1361     cx: &App,
   1362     content: impl IntoElement,
   1363 ) -> impl IntoElement {
   1364     let selected_background = rgb(APP_UI_THEME.foundation.surfaces.window_background);
   1365 
   1366     Button::new(id)
   1367         .custom(
   1368             ButtonCustomVariant::new(cx)
   1369                 .color(rgb(APP_UI_THEME.foundation.surfaces.card_background).into())
   1370                 .foreground(rgb(APP_UI_THEME.foundation.text.primary).into())
   1371                 .border(transparent_black())
   1372                 .hover(selected_background.into())
   1373                 .active(selected_background.into()),
   1374         )
   1375         .rounded(ButtonRounded::Size(px(APP_UI_THEME
   1376             .foundation
   1377             .radii
   1378             .medium_px)))
   1379         .w_full()
   1380         .on_click(on_click)
   1381         .child(
   1382             div()
   1383                 .w_full()
   1384                 .min_w_0()
   1385                 .bg(rgb(if is_selected {
   1386                     APP_UI_THEME.foundation.surfaces.window_background
   1387                 } else {
   1388                     APP_UI_THEME.foundation.surfaces.card_background
   1389                 }))
   1390                 .rounded(px(APP_UI_THEME.foundation.radii.medium_px))
   1391                 .child(content),
   1392         )
   1393 }
   1394 
   1395 fn app_button_base(
   1396     id: impl Into<ElementId>,
   1397     variant: AppButtonVariant,
   1398     on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
   1399     cx: &App,
   1400 ) -> Button {
   1401     let sizing = APP_UI_THEME.components.app_button.sizing;
   1402     let colors = app_button_colors(variant);
   1403     let hover_background = if colors.hover_changes_background {
   1404         colors.hover_background
   1405     } else {
   1406         colors.background
   1407     };
   1408 
   1409     Button::new(id)
   1410         .custom(
   1411             ButtonCustomVariant::new(cx)
   1412                 .color(rgb(colors.background).into())
   1413                 .foreground(rgb(colors.foreground).into())
   1414                 .border(transparent_black())
   1415                 .hover(rgb(hover_background).into())
   1416                 .active(rgb(colors.active_background).into()),
   1417         )
   1418         .rounded(ButtonRounded::Size(px(sizing.corner_radius_px)))
   1419         .h(px(sizing.height_px))
   1420         .on_click(on_click)
   1421 }
   1422 
   1423 fn app_button_base_disabled(
   1424     id: impl Into<ElementId>,
   1425     variant: AppButtonVariant,
   1426     cx: &App,
   1427 ) -> Button {
   1428     let sizing = APP_UI_THEME.components.app_button.sizing;
   1429     let colors = app_button_disabled_colors(variant);
   1430 
   1431     Button::new(id)
   1432         .custom(
   1433             ButtonCustomVariant::new(cx)
   1434                 .color(rgb(colors.background).into())
   1435                 .foreground(rgb(colors.foreground).into())
   1436                 .border(transparent_black())
   1437                 .hover(rgb(colors.hover_background).into())
   1438                 .active(rgb(colors.active_background).into()),
   1439         )
   1440         .rounded(ButtonRounded::Size(px(sizing.corner_radius_px)))
   1441         .h(px(sizing.height_px))
   1442 }
   1443 
   1444 fn app_button_colors(variant: AppButtonVariant) -> crate::AppButtonColors {
   1445     match variant {
   1446         AppButtonVariant::Secondary => APP_UI_THEME.components.app_button.secondary_colors,
   1447         AppButtonVariant::Primary => APP_UI_THEME.components.app_button.primary_colors,
   1448     }
   1449 }
   1450 
   1451 fn app_button_disabled_colors(variant: AppButtonVariant) -> crate::AppButtonColors {
   1452     match variant {
   1453         AppButtonVariant::Secondary | AppButtonVariant::Primary => {
   1454             APP_UI_THEME.components.app_button.primary_disabled_colors
   1455         }
   1456     }
   1457 }
   1458 
   1459 #[cfg(test)]
   1460 mod tests {
   1461     use gpui_component::IconName;
   1462 
   1463     use super::{
   1464         AppCheckboxFieldSpec, AppFormFieldSpec, AppIconButtonSpec, AppSegmentButtonIconSpec,
   1465     };
   1466 
   1467     #[test]
   1468     fn icon_segment_spec_preserves_id_and_label() {
   1469         let spec = AppSegmentButtonIconSpec::new("settings", "Settings", IconName::Settings2);
   1470 
   1471         assert_eq!(spec.id, "settings");
   1472         assert_eq!(spec.label.as_ref(), "Settings");
   1473     }
   1474 
   1475     #[test]
   1476     fn checkbox_field_spec_preserves_optional_note() {
   1477         let spec = AppCheckboxFieldSpec::new("launch", "Launch at login", Some("Optional note"));
   1478 
   1479         assert_eq!(spec.id, "launch");
   1480         assert_eq!(spec.label.as_ref(), "Launch at login");
   1481         assert_eq!(
   1482             spec.note.as_ref().map(|note| note.as_ref()),
   1483             Some("Optional note")
   1484         );
   1485     }
   1486 
   1487     #[test]
   1488     fn form_field_spec_preserves_optional_note() {
   1489         let spec = AppFormFieldSpec::new("Farm name", Some("Saved locally"));
   1490 
   1491         assert_eq!(spec.label.as_ref(), "Farm name");
   1492         assert_eq!(
   1493             spec.note.as_ref().map(|note| note.as_ref()),
   1494             Some("Saved locally")
   1495         );
   1496     }
   1497 
   1498     #[test]
   1499     fn icon_button_spec_preserves_id_label_and_icon() {
   1500         let spec = AppIconButtonSpec::new("more", "More actions", IconName::ChevronDown);
   1501 
   1502         assert_eq!(spec.id, "more");
   1503         assert_eq!(spec.label.as_ref(), "More actions");
   1504         assert!(matches!(spec.icon, IconName::ChevronDown));
   1505     }
   1506 }