1use gpui::{
2 App, Component, Hsla, IntoElement, MouseButton, RenderOnce, SharedString, Window, prelude::*,
3 px,
4};
5use liora_core::{Config, stable_unique_id};
6use liora_icons::Icon;
7use liora_icons_lucide::IconName;
8use liora_theme::{ButtonVariant, Theme};
9
10pub struct Link {
11 label: SharedString,
12 href: Option<SharedString>,
13 variant: ButtonVariant,
14 disabled: bool,
15 underline: bool,
16 icon_start: Option<IconName>,
17 icon_end: Option<IconName>,
18 id: Option<SharedString>,
19}
20
21impl Link {
22 pub fn new(label: impl Into<SharedString>) -> Self {
23 Self {
24 label: label.into(),
25 href: None,
26 variant: ButtonVariant::Default,
27 disabled: false,
28 underline: true,
29 icon_start: None,
30 icon_end: None,
31 id: None,
32 }
33 }
34 pub fn href(mut self, url: impl Into<SharedString>) -> Self {
35 self.href = Some(url.into());
36 self
37 }
38 pub fn variant(mut self, v: ButtonVariant) -> Self {
39 self.variant = v;
40 self
41 }
42 pub fn primary(mut self) -> Self {
43 self.variant = ButtonVariant::Primary;
44 self
45 }
46 pub fn success(mut self) -> Self {
47 self.variant = ButtonVariant::Success;
48 self
49 }
50 pub fn warning(mut self) -> Self {
51 self.variant = ButtonVariant::Warning;
52 self
53 }
54 pub fn danger(mut self) -> Self {
55 self.variant = ButtonVariant::Danger;
56 self
57 }
58 pub fn info(mut self) -> Self {
59 self.variant = ButtonVariant::Info;
60 self
61 }
62 pub fn disabled(mut self, d: bool) -> Self {
63 self.disabled = d;
64 self
65 }
66 pub fn underline(mut self, u: bool) -> Self {
67 self.underline = u;
68 self
69 }
70 pub fn icon_start(mut self, icon: IconName) -> Self {
71 self.icon_start = Some(icon);
72 self
73 }
74 pub fn icon_end(mut self, icon: IconName) -> Self {
75 self.icon_end = Some(icon);
76 self
77 }
78
79 pub fn id(mut self, id: impl Into<SharedString>) -> Self {
80 self.id = Some(id.into());
81 self
82 }
83
84 fn color_for(&self, theme: &Theme) -> (Hsla, Hsla) {
85 if self.disabled {
86 return (theme.neutral.text_disabled, theme.neutral.text_disabled);
87 }
88 let family = match self.variant {
89 ButtonVariant::Default | ButtonVariant::Tertiary | ButtonVariant::Text => {
90 &theme.primary
91 }
92 ButtonVariant::Primary => &theme.primary,
93 ButtonVariant::Success => &theme.success,
94 ButtonVariant::Warning => &theme.warning,
95 ButtonVariant::Danger => &theme.danger,
96 ButtonVariant::Info => &theme.info,
97 };
98 (family.base, family.hover)
99 }
100
101 fn render_with_theme(
102 self,
103 theme: Theme,
104 window: &mut Window,
105 cx: &mut App,
106 ) -> impl IntoElement {
107 let (color, hover_color) = self.color_for(&theme);
108 let fs = theme.font_size.md;
109 let icon_sz = 14.0;
110 let id = self.id.clone().unwrap_or_else(|| {
111 stable_unique_id(
112 format!(
113 "link:{}:{:?}:disabled={}:underline={}",
114 self.label, self.variant, self.disabled, self.underline
115 ),
116 "link",
117 window,
118 cx,
119 )
120 });
121
122 let mut div = gpui::div()
123 .flex()
124 .flex_row()
125 .items_center()
126 .gap_1()
127 .text_size(px(fs))
128 .text_color(color)
129 .id(id);
130
131 if !self.disabled {
132 div = div.cursor_pointer();
133 } else {
134 div = div.cursor_not_allowed();
135 }
136 if self.underline {
137 div = div.underline();
138 }
139
140 let mut children: Vec<Box<dyn FnOnce() -> gpui::AnyElement>> = Vec::new();
141 if let Some(icon) = self.icon_start {
142 children.push(Box::new(move || {
143 Icon::new(icon)
144 .size(px(icon_sz))
145 .color(color)
146 .into_any_element()
147 }));
148 }
149 let label = self.label.clone();
150 children.push(Box::new(move || {
151 gpui::div().child(label).into_any_element()
152 }));
153 if let Some(icon) = self.icon_end {
154 children.push(Box::new(move || {
155 Icon::new(icon)
156 .size(px(icon_sz))
157 .color(color)
158 .into_any_element()
159 }));
160 }
161
162 if !self.disabled {
163 if let Some(ref href) = self.href {
164 let url = href.clone();
165 div = div.on_mouse_up(MouseButton::Left, move |_, _, _| {
166 open_url(&url);
167 });
168 }
169 div = div.hover(move |style| style.text_color(hover_color));
170 }
171
172 div.children(children.into_iter().map(|f| f()))
173 }
174}
175
176fn open_url(url: &str) {
177 #[cfg(target_os = "linux")]
178 {
179 if let Err(e) = std::process::Command::new("xdg-open").arg(url).spawn() {
180 eprintln!("Link: failed to open URL: {}", e);
181 }
182 }
183 #[cfg(target_os = "macos")]
184 {
185 let _ = std::process::Command::new("open").arg(url).spawn();
186 }
187 #[cfg(target_os = "windows")]
188 {
189 let _ = std::process::Command::new("cmd")
190 .args(["/c", "start", "", url])
191 .spawn();
192 }
193}
194
195impl RenderOnce for Link {
196 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
197 let theme = cx.global::<Config>().theme.clone();
198 self.render_with_theme(theme, _window, cx)
199 }
200}
201
202impl IntoElement for Link {
203 type Element = Component<Self>;
204 fn into_element(self) -> Self::Element {
205 Component::new(self)
206 }
207}