diff --git a/src/navigation/tabs.rs b/src/navigation/tabs.rs index 9d8489d..d3c7e4b 100644 --- a/src/navigation/tabs.rs +++ b/src/navigation/tabs.rs @@ -2,7 +2,7 @@ use std::rc::Rc; use gpui::{ Animation, AnimationExt, AnyElement, App, ElementId, FontWeight, InteractiveElement, - IntoElement, MouseButton, ParentElement, RenderOnce, Role, SharedString, + IntoElement, MouseButton, ParentElement, Pixels, Point, RenderOnce, Role, SharedString, StatefulInteractiveElement, Styled, Window, div, prelude::*, }; @@ -23,6 +23,7 @@ pub enum TabPlacement { } type OnValue = Rc; +type OnContext = Rc, &mut Window, &mut App)>; /// A strip of tabs and the chosen tab's panel. A line slides to the chosen tab; arrows move it. #[derive(IntoElement)] @@ -33,6 +34,7 @@ pub struct Tabs { placement: TabPlacement, panel: Option, on_change: Option, + on_context: Option, } impl Tabs { @@ -48,6 +50,7 @@ impl Tabs { placement: TabPlacement::default(), panel: None, on_change: None, + on_context: None, } } @@ -71,6 +74,17 @@ impl Tabs { } } +impl Tabs { + /// A right click on a tab: its value and the pointer. + pub fn on_context( + mut self, + handler: impl Fn(&SharedString, Point, &mut Window, &mut App) + 'static, + ) -> Self { + self.on_context = Some(Rc::new(handler)); + self + } +} + impl RenderOnce for Tabs { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let values: Vec = self.tabs.iter().map(|tab| tab.value.clone()).collect(); @@ -148,6 +162,8 @@ impl RenderOnce for Tabs { (false, false) => colors.fg_muted, }; let pick = pick.clone(); + let context = self.on_context.clone().filter(|_| !tab.disabled); + let value = tab.value.clone(); div() .id(("tab", ix)) .role(Role::Tab) @@ -185,6 +201,14 @@ impl RenderOnce for Tabs { } }) }) + .when_some(context, |item, context| { + item.on_mouse_down(MouseButton::Right, move |event, window, cx| { + window.prevent_default(); + cx.stop_propagation(); + log::info!("tabs: context on {value}"); + context(&value, event.position, window, cx); + }) + }) .when_some(tab.icon, |item, icon| { item.child(Icon::new(icon).size(IconSize::Sm).color(fg)) }) diff --git a/src/navigation/tests.rs b/src/navigation/tests.rs index ec9615d..bb3a4d5 100644 --- a/src/navigation/tests.rs +++ b/src/navigation/tests.rs @@ -470,3 +470,41 @@ fn steps_fill_a_column_their_block_measures_by_content(cx: &mut TestAppContext) "the steps span the card inside its padding" ); } + +struct Context_ { + chosen: SharedString, + asked: Option, +} + +impl Render for Context_ { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let view = cx.entity(); + let tabs = [Choice::new("a", "A"), Choice::new("c", "C")]; + Tabs::new("tabs", tabs, self.chosen.clone()).on_context(move |value, _, _, cx| { + view.update(cx, |view, cx| { + view.asked = Some(value.clone()); + cx.notify(); + }) + }) + } +} + +#[gpui::test] +fn a_right_click_names_its_tab_and_keeps_the_choice(cx: &mut TestAppContext) { + setup(cx); + let (view, cx) = cx.add_window_view(|_, _| Context_ { + chosen: "c".into(), + asked: None, + }); + settle(cx); + let at = point(px(12.0), px(16.0)); + cx.simulate_mouse_move(at, None, Modifiers::none()); + cx.simulate_mouse_down(at, gpui::MouseButton::Right, Modifiers::none()); + let (chosen, asked) = view.read_with(cx, |view, _| (view.chosen.clone(), view.asked.clone())); + assert_eq!( + asked.as_deref(), + Some("a"), + "the clicked tab, not the chosen one" + ); + assert_eq!(chosen, "c"); +}