Skip to main content

egui_components/
link.rs

1//! `Link` — a themed hyperlink.
2//!
3//! Returns an [`egui::Response`] so it composes like any widget. Give it a
4//! [`url`](Link::url) to open in the browser on click, or leave it off and
5//! handle `.clicked()` yourself.
6//!
7//! ```ignore
8//! ui.add(sc::Link::new("Documentation").url("https://docs.rs"));
9//! if ui.add(sc::Link::new("Run action")).clicked() { /* … */ }
10//! ```
11
12use egui::{FontId, Response, Sense, Stroke, Ui, Widget};
13use egui_components_theme::Theme;
14
15use crate::common::Size;
16
17pub struct Link {
18    text: String,
19    url: Option<String>,
20    size: Size,
21    underline: UnderlineMode,
22}
23
24#[derive(Clone, Copy, PartialEq, Eq)]
25enum UnderlineMode {
26    OnHover,
27    Always,
28    Never,
29}
30
31impl Link {
32    pub fn new(text: impl Into<String>) -> Self {
33        Self {
34            text: text.into(),
35            url: None,
36            size: Size::Medium,
37            underline: UnderlineMode::OnHover,
38        }
39    }
40    pub fn url(mut self, url: impl Into<String>) -> Self {
41        self.url = Some(url.into());
42        self
43    }
44    pub fn size(mut self, s: Size) -> Self {
45        self.size = s;
46        self
47    }
48    pub fn underline(mut self) -> Self {
49        self.underline = UnderlineMode::Always;
50        self
51    }
52    pub fn no_underline(mut self) -> Self {
53        self.underline = UnderlineMode::Never;
54        self
55    }
56}
57
58impl Widget for Link {
59    fn ui(self, ui: &mut Ui) -> Response {
60        let theme = Theme::get(ui.ctx());
61        let c = theme.colors;
62        let font = FontId::proportional(self.size.font_size(&theme.metrics));
63
64        let galley = ui.ctx().fonts_mut(|f| {
65            f.layout_no_wrap(self.text.clone(), font, c.link_foreground)
66        });
67        let (rect, response) = ui.allocate_exact_size(galley.size(), Sense::click());
68
69        if ui.is_rect_visible(rect) {
70            let color = if response.is_pointer_button_down_on() {
71                c.link_active_foreground
72            } else if response.hovered() {
73                c.link_hover_foreground
74            } else {
75                c.link_foreground
76            };
77            let painter = ui.painter();
78            painter.galley_with_override_text_color(rect.min, galley.clone(), color);
79
80            let underline = match self.underline {
81                UnderlineMode::Always => true,
82                UnderlineMode::OnHover => response.hovered(),
83                UnderlineMode::Never => false,
84            };
85            if underline {
86                let y = rect.bottom() - 1.0;
87                painter.line_segment(
88                    [egui::pos2(rect.left(), y), egui::pos2(rect.right(), y)],
89                    Stroke::new(1.0, color),
90                );
91            }
92            if response.hovered() {
93                ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
94            }
95        }
96
97        if response.clicked() {
98            if let Some(url) = &self.url {
99                ui.ctx().open_url(egui::OpenUrl::new_tab(url));
100            }
101        }
102
103        response
104    }
105}