Skip to main content

gpui_macos/
link.rs

1//! Link presentation that suppresses transient styles while disabled.
2use gpui_kit::base::LinkStyles;
3use gpui_kit::{
4    AnyElement, App, ClickEvent, ElementId, InteractiveElement, Interactivity, IntoElement,
5    ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, StyleRefinement, Styled,
6    Window,
7};
8
9/// A macOS link backed by gpui-base's activation and focus behavior.
10/// Transient styles are applied only after the final disabled state is known.
11#[derive(IntoElement)]
12pub struct Link {
13    base: gpui_kit::base::Link,
14    disabled: bool,
15    hover: Option<Box<StyleRefinement>>,
16    active: Option<Box<StyleRefinement>>,
17    focus_visible: Option<Box<StyleRefinement>>,
18}
19
20impl Link {
21    pub fn new(id: impl Into<ElementId>) -> Self {
22        Self {
23            base: gpui_kit::base::Link::new(id).cursor_pointer(),
24            disabled: false,
25            hover: None,
26            active: None,
27            focus_visible: None,
28        }
29    }
30
31    pub fn disabled(mut self, disabled: bool) -> Self {
32        self.disabled = disabled;
33        self.base = self.base.disabled(disabled);
34        self
35    }
36
37    pub fn styles(mut self, build: impl FnOnce(LinkStyles) -> LinkStyles) -> Self {
38        self.base = self.base.styles(build);
39        self
40    }
41
42    pub fn accessibility_label(mut self, label: impl Into<SharedString>) -> Self {
43        self.base = self.base.accessibility_label(label);
44        self
45    }
46
47    pub fn tab_index(mut self, index: isize) -> Self {
48        self.base = self.base.tab_index(index);
49        self
50    }
51
52    pub fn tab_stop(mut self, tab_stop: bool) -> Self {
53        self.base = self.base.tab_stop(tab_stop);
54        self
55    }
56
57    pub fn href(mut self, href: impl Into<SharedString>) -> Self {
58        self.base = self.base.href(href);
59        self
60    }
61
62    pub fn open_with(
63        mut self,
64        open: impl Fn(&str, &ClickEvent, &mut Window, &mut App) + 'static,
65    ) -> Self {
66        self.base = self.base.open_with(open);
67        self
68    }
69
70    pub fn on_activate(
71        mut self,
72        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
73    ) -> Self {
74        self.base = self.base.on_activate(handler);
75        self
76    }
77}
78
79impl Styled for Link {
80    fn style(&mut self) -> &mut StyleRefinement {
81        self.base.style()
82    }
83}
84
85impl ParentElement for Link {
86    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
87        self.base.extend(elements);
88    }
89}
90
91impl InteractiveElement for Link {
92    fn interactivity(&mut self) -> &mut Interactivity {
93        self.base.interactivity()
94    }
95
96    fn hover(mut self, build: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self {
97        self.hover = Some(Box::new(build(StyleRefinement::default())));
98        self
99    }
100    fn focus_visible(mut self, build: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self {
101        self.focus_visible = Some(Box::new(build(StyleRefinement::default())));
102        self
103    }
104}
105
106impl StatefulInteractiveElement for Link {
107    fn active(mut self, build: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self {
108        self.active = Some(Box::new(build(StyleRefinement::default())));
109        self
110    }
111}
112
113impl RenderOnce for Link {
114    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
115        let mut base = self.base;
116        if self.disabled {
117            base = base.cursor_default();
118        } else {
119            if let Some(style) = self.hover {
120                base = base.hover(|_| *style);
121            }
122            if let Some(style) = self.active {
123                base = base.active(|_| *style);
124            }
125            if let Some(style) = self.focus_visible {
126                base = base.focus_visible(|_| *style);
127            }
128        }
129        base
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use gpui_kit::px;
137    use gpui_kit::rems;
138    use gpui_kit::{Context, Modifiers, MouseButton, Render, TestAppContext, div, point};
139    #[gpui_kit::test]
140    fn disabled_link_suppresses_hover_and_pressed_geometry(cx: &mut TestAppContext) {
141        cx.update(crate::init);
142        struct States {
143            disabled: bool,
144            disabled_first: bool,
145        }
146        impl Render for States {
147            fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
148                let link = crate::link("link", "Project", "https://example.com", cx)
149                    .w(rems(6.25))
150                    .h(rems(2.5));
151                let link = if self.disabled_first {
152                    link.disabled(self.disabled)
153                        .hover(|s| s.w(rems(10.)))
154                        .active(|s| s.w(rems(12.5)))
155                } else {
156                    link.hover(|s| s.w(rems(10.)))
157                        .active(|s| s.w(rems(12.5)))
158                        .disabled(self.disabled)
159                };
160                div()
161                    .size(rems(25.))
162                    .child(link.debug_selector(|| "state-link".into()))
163            }
164        }
165        for disabled_first in [false, true] {
166            let (view, cx) = cx.add_window_view(move |_, _| States {
167                disabled: true,
168                disabled_first,
169            });
170            cx.update(|window, cx| window.draw(cx).clear(cx));
171            let position = point(px(10.), px(10.));
172            cx.simulate_mouse_move(position, None, Modifiers::default());
173            cx.update(|window, cx| window.draw(cx).clear(cx));
174            assert_eq!(cx.debug_bounds("state-link").unwrap().size.width, px(100.));
175            cx.simulate_mouse_down(position, MouseButton::Left, Modifiers::default());
176            cx.update(|window, cx| window.draw(cx).clear(cx));
177            assert_eq!(cx.debug_bounds("state-link").unwrap().size.width, px(100.));
178            cx.simulate_mouse_up(position, MouseButton::Left, Modifiers::default());
179            view.update(cx, |this, cx| {
180                this.disabled = false;
181                cx.notify();
182            });
183            cx.update(|window, cx| window.draw(cx).clear(cx));
184            cx.simulate_mouse_move(point(px(300.), px(300.)), None, Modifiers::default());
185            cx.simulate_mouse_move(position, None, Modifiers::default());
186            cx.update(|window, cx| window.draw(cx).clear(cx));
187            assert_eq!(cx.debug_bounds("state-link").unwrap().size.width, px(160.));
188            cx.simulate_mouse_down(position, MouseButton::Left, Modifiers::default());
189            cx.update(|window, cx| window.draw(cx).clear(cx));
190            assert_eq!(cx.debug_bounds("state-link").unwrap().size.width, px(200.));
191            cx.simulate_mouse_up(position, MouseButton::Left, Modifiers::default());
192        }
193    }
194}