egui/widgets/
hyperlink.rs

1use crate::{
2    epaint, text_selection, CursorIcon, Label, Response, Sense, Stroke, Ui, Widget, WidgetInfo,
3    WidgetText, WidgetType,
4};
5
6use self::text_selection::LabelSelectionState;
7
8/// Clickable text, that looks like a hyperlink.
9///
10/// To link to a web page, use [`Hyperlink`], [`Ui::hyperlink`] or [`Ui::hyperlink_to`].
11///
12/// See also [`Ui::link`].
13///
14/// ```
15/// # egui::__run_test_ui(|ui| {
16/// // These are equivalent:
17/// if ui.link("Documentation").clicked() {
18///     // …
19/// }
20///
21/// if ui.add(egui::Link::new("Documentation")).clicked() {
22///     // …
23/// }
24/// # });
25/// ```
26#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
27pub struct Link {
28    text: WidgetText,
29}
30
31impl Link {
32    pub fn new(text: impl Into<WidgetText>) -> Self {
33        Self { text: text.into() }
34    }
35}
36
37impl Widget for Link {
38    fn ui(self, ui: &mut Ui) -> Response {
39        let Self { text } = self;
40        let label = Label::new(text).sense(Sense::click());
41
42        let (galley_pos, galley, response) = label.layout_in_ui(ui);
43        response
44            .widget_info(|| WidgetInfo::labeled(WidgetType::Link, ui.is_enabled(), galley.text()));
45
46        if ui.is_rect_visible(response.rect) {
47            let color = ui.visuals().hyperlink_color;
48            let visuals = ui.style().interact(&response);
49
50            let underline = if response.hovered() || response.has_focus() {
51                Stroke::new(visuals.fg_stroke.width, color)
52            } else {
53                Stroke::NONE
54            };
55
56            let selectable = ui.style().interaction.selectable_labels;
57            if selectable {
58                LabelSelectionState::label_text_selection(
59                    ui, &response, galley_pos, galley, color, underline,
60                );
61            } else {
62                ui.painter().add(
63                    epaint::TextShape::new(galley_pos, galley, color).with_underline(underline),
64                );
65            }
66
67            if response.hovered() {
68                ui.ctx().set_cursor_icon(CursorIcon::PointingHand);
69            }
70        }
71
72        response
73    }
74}
75
76/// A clickable hyperlink, e.g. to `"https://github.com/emilk/egui"`.
77///
78/// See also [`Ui::hyperlink`] and [`Ui::hyperlink_to`].
79///
80/// ```
81/// # egui::__run_test_ui(|ui| {
82/// // These are equivalent:
83/// ui.hyperlink("https://github.com/emilk/egui");
84/// ui.add(egui::Hyperlink::new("https://github.com/emilk/egui"));
85///
86/// // These are equivalent:
87/// ui.hyperlink_to("My favorite repo", "https://github.com/emilk/egui");
88/// ui.add(egui::Hyperlink::from_label_and_url("My favorite repo", "https://github.com/emilk/egui"));
89/// # });
90/// ```
91#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
92pub struct Hyperlink {
93    url: String,
94    text: WidgetText,
95    new_tab: bool,
96}
97
98impl Hyperlink {
99    #[allow(clippy::needless_pass_by_value)]
100    pub fn new(url: impl ToString) -> Self {
101        let url = url.to_string();
102        Self {
103            url: url.clone(),
104            text: url.into(),
105            new_tab: false,
106        }
107    }
108
109    #[allow(clippy::needless_pass_by_value)]
110    pub fn from_label_and_url(text: impl Into<WidgetText>, url: impl ToString) -> Self {
111        Self {
112            url: url.to_string(),
113            text: text.into(),
114            new_tab: false,
115        }
116    }
117
118    /// Always open this hyperlink in a new browser tab.
119    #[inline]
120    pub fn open_in_new_tab(mut self, new_tab: bool) -> Self {
121        self.new_tab = new_tab;
122        self
123    }
124}
125
126impl Widget for Hyperlink {
127    fn ui(self, ui: &mut Ui) -> Response {
128        let Self { url, text, new_tab } = self;
129
130        let response = ui.add(Link::new(text));
131
132        if response.clicked() {
133            let modifiers = ui.ctx().input(|i| i.modifiers);
134            ui.ctx().open_url(crate::OpenUrl {
135                url: url.clone(),
136                new_tab: new_tab || modifiers.any(),
137            });
138        }
139        if response.middle_clicked() {
140            ui.ctx().open_url(crate::OpenUrl {
141                url: url.clone(),
142                new_tab: true,
143            });
144        }
145
146        if ui.style().url_in_tooltip {
147            response.on_hover_text(url)
148        } else {
149            response
150        }
151    }
152}