1use gpui::{
7 div, prelude::*, px, AnyElement, App, ClickEvent, ElementId, IntoElement, ParentElement,
8 Pixels, RenderOnce, SharedString, Styled, Window,
9};
10use herogpui_core::{element_id, Orientation as SelectionOrientation, SelectionMode, Size};
11use herogpui_theme::ActiveTheme;
12
13use crate::a11y::{self, A11y as _};
14
15#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
20pub enum ToggleVariant {
21 #[default]
22 Default,
23 Ghost,
24}
25
26#[derive(Clone, Default)]
27struct ToggleGroupFocusState {
28 last_key: Option<SharedString>,
29 was_inside: bool,
30 restore_on_entry: bool,
31 edge_exit: bool,
32}
33
34#[derive(IntoElement)]
35pub struct ToggleButton {
36 id: ElementId,
37 key: Option<SharedString>,
40 label: Option<SharedString>,
41 content: Option<std::sync::Arc<dyn Fn(crate::util::InteractiveState) -> AnyElement + 'static>>,
44 variant: ToggleVariant,
45 size: Size,
46 size_explicit: bool,
49 is_selected: Option<bool>,
52 default_selected: bool,
53 is_icon_only: bool,
54 group_focus_handle: Option<gpui::FocusHandle>,
57 group_single_selection: bool,
61 group_edge: Option<(crate::button::GroupEdge, bool)>,
65 is_disabled: bool,
66 disabled_explicit: bool,
67 children: Vec<AnyElement>,
68 hover_bg: Option<gpui::Hsla>,
70 radius: Option<Pixels>,
73 on_press: Option<std::sync::Arc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
76 on_change: Option<std::sync::Arc<dyn Fn(&bool, &mut Window, &mut App) + 'static>>,
79 sx: Option<Box<gpui::StyleRefinement>>,
81}
82
83impl ToggleButton {
84 pub fn key(mut self, key: impl Into<SharedString>) -> Self {
86 self.key = Some(key.into());
87 self
88 }
89
90 pub fn selection_key(&self) -> SharedString {
92 self.key.clone().unwrap_or_else(|| match &self.id {
93 ElementId::Name(name) => name.clone(),
94 other => other.to_string().into(),
95 })
96 }
97
98 pub fn new(id: impl Into<ElementId>) -> Self {
99 Self {
100 content: None,
101 id: id.into(),
102 key: None,
103 label: None,
104 variant: ToggleVariant::Default,
105 size: Size::Md,
106 size_explicit: false,
107 is_selected: None,
108 default_selected: false,
109 is_icon_only: false,
110 group_focus_handle: None,
111 group_single_selection: false,
112 group_edge: None,
113 is_disabled: false,
114 disabled_explicit: false,
115 children: Vec::new(),
116 on_press: None,
117 on_change: None,
118 sx: None,
119 hover_bg: None,
120 radius: None,
121 }
122 }
123
124 pub fn content(
129 mut self,
130 render: impl Fn(crate::util::InteractiveState) -> AnyElement + 'static,
131 ) -> Self {
132 self.content = Some(std::sync::Arc::new(render));
133 self
134 }
135
136 pub fn label(mut self, l: impl Into<SharedString>) -> Self {
137 self.label = Some(l.into());
138 self
139 }
140
141 pub fn variant(mut self, v: ToggleVariant) -> Self {
142 self.variant = v;
143 self
144 }
145
146 pub fn size(mut self, s: Size) -> Self {
147 self.size = s;
148 self.size_explicit = true;
149 self
150 }
151
152 pub fn hover_bg(mut self, color: impl Into<gpui::Hsla>) -> Self {
157 self.hover_bg = Some(color.into());
158 self
159 }
160
161 pub fn radius(mut self, radius: impl Into<Pixels>) -> Self {
166 self.radius = Some(radius.into());
167 self
168 }
169
170 pub fn sx(mut self, style: impl FnOnce(gpui::Div) -> gpui::Div) -> Self {
175 self.sx = Some(crate::util::capture_sx(style));
176 self
177 }
178
179 fn group_size(mut self, size: Size) -> Self {
180 if !self.size_explicit {
181 self.size = size;
182 }
183 self
184 }
185
186 pub fn is_selected(mut self, v: bool) -> Self {
187 self.is_selected = Some(v);
188 self
189 }
190
191 pub fn default_selected(mut self, v: bool) -> Self {
197 self.default_selected = v;
198 self
199 }
200
201 pub fn is_icon_only(mut self, v: bool) -> Self {
202 self.is_icon_only = v;
203 self
204 }
205
206 pub(crate) fn group_edge(mut self, edge: crate::button::GroupEdge, vertical: bool) -> Self {
209 self.group_edge = Some((edge, vertical));
210 self
211 }
212
213 fn group_focus_handle(mut self, handle: gpui::FocusHandle) -> Self {
214 self.group_focus_handle = Some(handle);
215 self
216 }
217
218 fn group_managed(mut self) -> Self {
219 self.on_change = None;
220 self
221 }
222
223 fn group_selection_mode(mut self, mode: SelectionMode) -> Self {
226 self.group_single_selection = mode == SelectionMode::Single;
227 self
228 }
229
230 fn group_on_press(
231 mut self,
232 handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
233 ) -> Self {
234 let child = self.on_press.take();
235 self.on_change = None;
236 self.on_press = Some(std::sync::Arc::new(move |event, window, cx| {
237 handler(event, window, cx);
238 if let Some(child) = &child {
239 child(event, window, cx);
240 }
241 }));
242 self
243 }
244
245 pub fn is_disabled(mut self, v: bool) -> Self {
246 self.is_disabled = v;
247 self.disabled_explicit = true;
248 self
249 }
250
251 fn group_disabled(mut self, v: bool) -> Self {
252 if !self.disabled_explicit {
253 self.is_disabled = v;
254 }
255 self
256 }
257
258 pub fn child(mut self, el: impl IntoElement) -> Self {
259 self.children.push(el.into_any_element());
260 self
261 }
262
263 pub fn on_change(mut self, f: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
267 self.on_change = Some(std::sync::Arc::new(f));
268 self
269 }
270
271 pub fn on_press(mut self, f: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self {
272 self.on_press = Some(std::sync::Arc::new(f));
273 self
274 }
275}
276
277impl ParentElement for ToggleButton {
278 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
279 self.children.extend(elements);
280 }
281}
282
283impl RenderOnce for ToggleButton {
284 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
285 let (is_selected, own) = crate::util::controlled(
287 window,
288 cx,
289 element_id::scoped(&self.id, "selected"),
290 self.is_selected,
291 self.default_selected,
292 );
293
294 let focus_handle = self.group_focus_handle.clone().unwrap_or_else(|| {
296 crate::util::tab_stop_handle(element_id::scoped(&self.id, "focus"), window, cx)
297 });
298 let interaction = self.content.as_ref().map(|_| {
300 crate::util::interaction(element_id::scoped(&self.id, "interaction"), window, cx)
301 });
302 let colors = cx.colors().clone();
303 let sem = colors.accent;
304 let disabled_opacity = cx.layout().disabled_opacity;
305 let radius = self
306 .radius
307 .unwrap_or_else(|| crate::util::control_radius(cx));
308 let is_grouped = self.group_edge.is_some();
309
310 let sx_background = crate::util::sx_background(&self.sx);
311 let sx_corners = crate::util::sx_radius(&self.sx);
312 let fade = (!self.is_disabled)
313 .then(|| {
314 let idle = if is_selected {
315 sem.soft()
316 } else {
317 match self.variant {
318 ToggleVariant::Default => colors.default.color,
319 ToggleVariant::Ghost => gpui::transparent_black(),
320 }
321 };
322 let hover = if is_selected {
323 colors.accent.soft_hover()
324 } else {
325 match self.variant {
326 ToggleVariant::Default => colors.default.hover(),
327 ToggleVariant::Ghost => colors.default.color,
328 }
329 };
330 (idle, hover)
331 })
332 .and_then(|pair| crate::util::fade_endpoints(Some(pair), sx_background, self.hover_bg));
333
334 let single = self.group_single_selection;
338 let name = a11y::Name::maybe(self.label.clone());
339 let mut el = div()
340 .id(self.id.clone())
341 .map(|e| {
342 if single {
343 e.a11y_named(a11y::Role::RadioButton, &name)
344 .a11y_checked(is_selected, false)
345 } else {
346 e.a11y_named(a11y::Role::Button, &name)
347 .a11y_pressed(is_selected)
348 }
349 })
350 .flex()
351 .items_center()
352 .justify_center()
353 .whitespace_nowrap()
360 .flex_shrink_0()
361 .font_weight(gpui::FontWeight::MEDIUM)
362 .when(is_selected, |e| {
363 let e = if fade.is_none() { e.bg(sem.soft()) } else { e };
364 e.text_color(sem.soft_foreground(colors.foreground))
365 })
366 .when(!is_selected, |e| match self.variant {
367 ToggleVariant::Default => {
368 let e = if fade.is_none() {
369 e.bg(colors.default.color)
370 } else {
371 e
372 };
373 e.text_color(colors.foreground)
374 }
375 ToggleVariant::Ghost => {
376 let e = if fade.is_none() {
377 e.bg(gpui::transparent_black())
378 } else {
379 e
380 };
381 e.text_color(colors.default.foreground)
382 }
383 });
384
385 let (height, pad_x, gap, press_scale) = match self.size {
391 Size::Sm => (px(32.), px(12.), px(8.), crate::anim::PRESSED_SCALE_SUBTLE),
392 Size::Md => (px(36.), px(16.), px(8.), crate::anim::PRESSED_SCALE),
393 Size::Lg => (px(40.), px(16.), px(8.), crate::anim::PRESSED_SCALE_FIRM),
394 };
395 let (text, line) = match self.size {
396 Size::Sm | Size::Md => (px(14.), px(20.)),
397 Size::Lg => (px(16.), px(24.)),
398 };
399 el = el.h(height).text_size(text).line_height(line);
400 el = if self.is_icon_only {
401 el.w(height)
402 } else {
403 el.px(pad_x).gap(gap)
404 };
405
406 el = crate::button::group_radius_any(el, self.group_edge, radius);
407 el = crate::util::round_sx_corners(el, &sx_corners);
408
409 if let Some(colors) = fade {
410 let edge = self.group_edge;
411 el = crate::anim::hover_fade(
412 el,
413 element_id::scoped(&self.id, "fade"),
414 colors,
415 interaction.as_ref(),
416 None,
417 move |fill| {
418 crate::util::round_sx_corners(
419 crate::button::group_radius_any(fill, edge, radius),
420 &sx_corners,
421 )
422 },
423 window,
424 cx,
425 );
426 }
427
428 if self.is_disabled {
429 el = el.opacity(disabled_opacity);
430 }
431
432 if let Some(render) = self.content.clone() {
433 let (is_hovered, is_pressed) = interaction
434 .as_ref()
435 .map(|slot| *slot.read(cx))
436 .unwrap_or_default();
437 let focused = focus_handle.is_focused(window);
438 el = el.child(render(crate::util::InteractiveState {
439 is_hovered,
440 is_pressed,
441 is_focused: focused,
442 is_focus_visible: focused && crate::util::focus_visible(cx),
443 is_selected,
444 is_disabled: self.is_disabled,
445 is_pending: false,
446 is_indeterminate: false,
447 }));
448 } else if let Some(label) = self.label {
449 el = el.child(label.to_string());
450 }
451 el = el.children(self.children);
455
456 if !self.is_disabled {
460 let hover_bg = if is_selected {
461 colors.accent.soft_hover()
462 } else {
463 match self.variant {
464 ToggleVariant::Default => colors.default.hover(),
465 ToggleVariant::Ghost => colors.default.color,
466 }
467 };
468 el = crate::util::cursor_interactive(el, cx);
469 if fade.is_none() {
470 el = el.hover(move |s| s.bg(hover_bg));
471 }
472 let press_endpoints = fade.map(|(idle, _)| (idle, hover_bg));
482 if is_grouped {
483 el = el.active(move |style| style.bg(hover_bg));
484 } else {
485 el = crate::anim::pressed_with_background_ramp(
486 el,
487 crate::anim::PressBox {
488 height,
489 padding_x: (!self.is_icon_only).then_some(pad_x),
490 width: self.is_icon_only.then_some(height),
491 min_width: None,
492 text_size: text,
493 line_height: line,
494 gap,
495 radius,
496 shrink_x: true,
497 scale: press_scale,
498 },
499 press_endpoints,
500 crate::anim::BUTTON_PRESS,
501 interaction.as_ref(),
502 window,
503 cx,
504 );
505 }
506 }
507
508 if let Some(slot) = &interaction {
509 el = crate::util::track_interaction(el, slot);
510 }
511
512 if !self.is_disabled
513 && (self.on_press.is_some() || self.on_change.is_some() || own.is_some())
514 {
515 let on_press = self.on_press;
516 let on_change = self.on_change;
517 let next = !is_selected;
518 el = el.on_click(move |ev, w, cx| {
519 if let Some(held) = &own {
522 held.update(cx, |v, cx| {
523 *v = next;
524 cx.notify();
525 });
526 }
527 if let Some(cb) = &on_change {
528 cb(&next, w, cx);
529 }
530 if let Some(cb) = &on_press {
531 cb(ev, w, cx);
532 }
533 });
534 }
535
536 if self.is_disabled {
537 return crate::util::apply_sx(el, &self.sx);
538 }
539 let el = match crate::button::uniform_ring_radius(self.group_edge, radius, &sx_corners) {
547 Some(ring_radius) => crate::util::ring_overlay_if_focused(
548 el.track_focus(&focus_handle),
549 &focus_handle,
550 !is_grouped,
551 ring_radius,
552 Vec::new(),
553 window,
554 cx,
555 ),
556 None => crate::util::ring_if_focused(
557 el.track_focus(&focus_handle),
558 &focus_handle,
559 !is_grouped,
560 Vec::new(),
561 window,
562 cx,
563 ),
564 };
565 crate::util::apply_sx(el, &self.sx)
566 }
567}
568
569#[derive(IntoElement)]
574pub struct ToggleButtonGroup {
575 id: ElementId,
576 selected: Vec<SharedString>,
577 default_selected: Vec<SharedString>,
578 is_controlled: bool,
580 selection_mode: SelectionMode,
581 size: Size,
582 is_disabled: bool,
583 is_detached: bool,
584 separators: bool,
587 is_vertical: bool,
588 disallow_empty_selection: bool,
589 full_width: bool,
590 children: Vec<ToggleButton>,
591 on_change: Option<std::sync::Arc<dyn Fn(&[SharedString], &mut Window, &mut App) + 'static>>,
592 sx: Option<Box<gpui::StyleRefinement>>,
594}
595
596impl ToggleButtonGroup {
597 pub fn orientation(mut self, orientation: SelectionOrientation) -> Self {
599 self.is_vertical = orientation == SelectionOrientation::Vertical;
600 self
601 }
602
603 pub fn disallow_empty_selection(mut self, v: bool) -> Self {
605 self.disallow_empty_selection = v;
606 self
607 }
608
609 pub fn on_selection_change(
611 self,
612 handler: impl Fn(&[SharedString], &mut Window, &mut App) + 'static,
613 ) -> Self {
614 self.on_change(handler)
615 }
616
617 pub fn new(id: impl Into<ElementId>) -> Self {
618 Self {
619 id: id.into(),
620 selected: Vec::new(),
621 default_selected: Vec::new(),
622 is_controlled: false,
623 selection_mode: SelectionMode::Single,
624 size: Size::Md,
625 is_disabled: false,
626 is_detached: false,
627 separators: false,
628 is_vertical: false,
629 disallow_empty_selection: false,
630 full_width: false,
631 children: Vec::new(),
632 on_change: None,
633 sx: None,
634 }
635 }
636
637 pub fn selection_mode(mut self, m: SelectionMode) -> Self {
638 self.selection_mode = m;
639 self
640 }
641
642 pub fn size(mut self, size: Size) -> Self {
644 self.size = size;
645 self
646 }
647
648 pub fn is_disabled(mut self, v: bool) -> Self {
650 self.is_disabled = v;
651 self
652 }
653
654 pub fn separators(mut self, v: bool) -> Self {
656 self.separators = v;
657 self
658 }
659
660 pub fn is_detached(mut self, v: bool) -> Self {
661 self.is_detached = v;
662 self
663 }
664
665 pub fn full_width(mut self, v: bool) -> Self {
666 self.full_width = v;
667 self
668 }
669
670 pub fn sx(mut self, style: impl FnOnce(gpui::Div) -> gpui::Div) -> Self {
676 self.sx = Some(crate::util::capture_sx(style));
677 self
678 }
679
680 pub fn selected_keys(
681 mut self,
682 keys: impl IntoIterator<Item = impl Into<SharedString>>,
683 ) -> Self {
684 self.selected = keys.into_iter().map(Into::into).collect();
685 self.is_controlled = true;
686 self
687 }
688
689 pub fn default_selected_keys(
691 mut self,
692 keys: impl IntoIterator<Item = impl Into<SharedString>>,
693 ) -> Self {
694 self.default_selected = keys.into_iter().map(Into::into).collect();
695 self
696 }
697
698 pub fn on_change(
699 mut self,
700 f: impl Fn(&[SharedString], &mut Window, &mut App) + 'static,
701 ) -> Self {
702 self.on_change = Some(std::sync::Arc::new(f));
703 self
704 }
705
706 pub fn child_toggle(mut self, btn: ToggleButton) -> Self {
707 self.children.push(btn);
708 self
709 }
710}
711
712impl RenderOnce for ToggleButtonGroup {
713 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
714 let (selected, selection_own) = crate::util::controlled(
715 window,
716 cx,
717 element_id::scoped(&self.id, "selected"),
718 self.is_controlled.then_some(self.selected),
719 self.default_selected,
720 );
721 let gap = if self.is_detached { px(4.) } else { px(0.) };
725 let orientation = if self.is_vertical {
729 SelectionOrientation::Vertical
730 } else {
731 SelectionOrientation::Horizontal
732 };
733 let group_role = if self.selection_mode == SelectionMode::Single {
734 a11y::Role::RadioGroup
735 } else {
736 a11y::Role::Toolbar
737 };
738 let mut row = div()
739 .id(self.id.clone())
740 .a11y(group_role)
741 .a11y_orientation(orientation)
742 .flex()
743 .items_center()
744 .justify_center()
745 .when(self.is_vertical, |r| r.flex_col())
746 .when(!self.is_vertical, |r| r.flex_row())
747 .gap(gap)
748 .when(self.full_width, |r| r.w_full());
749 let total = self.children.len();
750 let separators = self.separators && !self.is_detached;
751 let is_vertical = self.is_vertical;
752 let full_width = self.full_width;
753 let separator_color = cx.colors().foreground.alpha(0.15);
758 let separator_radius = crate::util::hairline_radius(cx);
759
760 let mode = self.selection_mode;
761 let disallow_empty = self.disallow_empty_selection;
762
763 let mut children = self
764 .children
765 .into_iter()
766 .map(|button| {
767 button
768 .group_managed()
769 .group_selection_mode(mode)
770 .group_disabled(self.is_disabled)
771 .group_size(self.size)
772 })
773 .collect::<Vec<_>>();
774 let members = children
775 .iter()
776 .filter(|button| !button.is_disabled)
777 .map(|button| {
778 (
779 button.selection_key(),
780 crate::util::tab_stop_handle(
781 element_id::scoped(
782 &element_id::scoped(
783 &element_id::scoped(&self.id, "member"),
784 format!("{:?}", button.id),
785 ),
786 "focus",
787 ),
788 window,
789 cx,
790 ),
791 )
792 })
793 .collect::<Vec<_>>();
794 let focus_state =
795 window.use_keyed_state(element_id::scoped(&self.id, "focus-state"), cx, |_, _| {
796 ToggleGroupFocusState::default()
797 });
798 let current = members
799 .iter()
800 .position(|(_, handle)| handle.is_focused(window));
801 let snapshot = focus_state.read(cx).clone();
802 if let Some(current) = current {
803 let mut effective = current;
804 if !snapshot.was_inside && snapshot.restore_on_entry {
805 if let Some(last) = &snapshot.last_key {
806 if let Some(restored) = members.iter().position(|(key, _)| key == last) {
807 effective = restored;
808 if restored != current {
809 window.focus(&members[restored].1, cx);
810 }
811 }
812 }
813 }
814 let key = members[effective].0.clone();
815 focus_state.update(cx, |state, _| {
816 state.last_key = Some(key);
817 state.was_inside = true;
818 state.restore_on_entry = false;
819 state.edge_exit = false;
820 });
821 } else if snapshot.was_inside {
822 focus_state.update(cx, |state, _| {
823 state.was_inside = false;
824 state.restore_on_entry = !state.edge_exit;
825 state.edge_exit = false;
826 });
827 } else if snapshot.edge_exit {
828 focus_state.update(cx, |state, _| state.edge_exit = false);
829 }
830
831 let vertical = self.is_vertical;
832 let key_focuses = members
833 .iter()
834 .map(|(_, handle)| handle.clone())
835 .collect::<Vec<_>>();
836 let key_focus_state = focus_state.clone();
837 row = row.on_key_down(move |event: &gpui::KeyDownEvent, window, cx| {
838 let movement = match (vertical, event.keystroke.key.as_str()) {
839 (false, "right") | (true, "down") => Some("next"),
840 (false, "left") | (true, "up") => Some("prev"),
841 _ => None,
842 };
843
844 if let Some(movement) = movement {
845 if window
849 .context_stack()
850 .iter()
851 .any(|context| context.contains("Toolbar"))
852 {
853 key_focus_state.update(cx, |state, _| state.edge_exit = true);
854 window.refresh();
855 return;
856 }
857 cx.stop_propagation();
861 let Some(index) = key_focuses
862 .iter()
863 .position(|handle| handle.is_focused(window))
864 else {
865 return;
866 };
867 let next = if movement == "next" {
868 index
869 .checked_add(1)
870 .filter(|next| *next < key_focuses.len())
871 } else {
872 index.checked_sub(1)
873 };
874 if let Some(next) = next {
875 window.focus(&key_focuses[next], cx);
876 }
877 return;
878 }
879
880 if matches!(
881 event.keystroke.key.as_str(),
882 "up" | "down" | "left" | "right"
883 ) {
884 key_focus_state.update(cx, |state, _| state.edge_exit = true);
887 window.refresh();
888 return;
889 }
890
891 if event.keystroke.key != "tab" {
892 return;
893 }
894 let edge = if event.keystroke.modifiers.shift {
898 key_focuses.first()
899 } else {
900 key_focuses.last()
901 };
902 if let Some(edge) = edge {
903 window.focus(edge, cx);
904 }
905 });
906
907 let mut member_focuses = members.into_iter().map(|(_, handle)| handle);
908 for (i, btn) in children.drain(..).enumerate() {
909 let key = btn.selection_key();
912 let is_selected = selected.iter().any(|k| k == &key);
913 let mut btn = if btn.is_disabled {
914 btn.is_selected(is_selected)
915 } else {
916 btn.group_focus_handle(
917 member_focuses
918 .next()
919 .expect("every enabled toggle has a group focus handle"),
920 )
921 .is_selected(is_selected)
922 };
923
924 if self.on_change.is_some() || selection_own.is_some() {
925 let on_change = self.on_change.clone();
926 let own = selection_own.clone();
927 let current = selected.clone();
928 let key = key.clone();
929 btn = btn.group_on_press(move |_, window, cx| {
930 let next =
931 crate::selection::next_selection(¤t, &key, mode, disallow_empty);
932 if let Some(held) = &own {
933 let held_next = next.clone();
934 held.update(cx, |value, cx| {
935 *value = held_next;
936 cx.notify();
937 });
938 }
939 if let Some(change) = &on_change {
940 change(&next, window, cx);
941 }
942 });
943 }
944
945 let edge = if self.is_detached || total <= 1 {
948 crate::button::GroupEdge::Only
949 } else if i == 0 {
950 crate::button::GroupEdge::Start
951 } else if i + 1 == total {
952 crate::button::GroupEdge::End
953 } else {
954 crate::button::GroupEdge::Middle
955 };
956 let mut slot = div()
957 .relative()
958 .child(btn.group_edge(edge, is_vertical))
959 .when(full_width, |sl| sl.flex_1());
960 if separators && i > 0 {
961 slot = slot.child(
962 div()
963 .absolute()
964 .bg(separator_color)
965 .rounded(separator_radius)
966 .map(|sep| {
967 if is_vertical {
968 sep.left(gpui::relative(0.25))
969 .top(px(-1.))
970 .w(gpui::relative(0.5))
971 .h(px(1.))
972 } else {
973 sep.left(px(-1.))
974 .top(gpui::relative(0.25))
975 .w(px(1.))
976 .h(gpui::relative(0.5))
977 }
978 }),
979 );
980 }
981 row = row.child(slot);
982 }
983
984 row = crate::util::apply_sx(row, &self.sx);
985 row
986 }
987}
988
989#[derive(IntoElement)]
991pub struct ToggleSeparator;
992
993impl RenderOnce for ToggleSeparator {
994 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
995 div().w(px(1.)).h(px(20.)).bg(cx.colors().separator)
996 }
997}
998
999#[cfg(test)]
1000mod tests {
1001 use super::*;
1002
1003 #[test]
1004 fn selection_key_defaults_to_the_element_id() {
1005 let plain = ToggleButton::new("bold");
1008 assert_eq!(plain.selection_key().as_ref(), "bold");
1009 let keyed = ToggleButton::new("grp-bold").key("bold");
1010 assert_eq!(keyed.selection_key().as_ref(), "bold");
1011 }
1012
1013 #[test]
1014 fn group_separators_are_explicit_composition() {
1015 assert!(
1016 !ToggleButtonGroup::new("plain").separators,
1017 "v3 groups without a Separator child must not synthesize dividers"
1018 );
1019 assert!(
1020 ToggleButtonGroup::new("separated")
1021 .separators(true)
1022 .separators
1023 );
1024 }
1025}