1use 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#[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}