Skip to main content

liora_components/
link.rs

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}