1use std::sync::Arc;
4
5use crate::callback::{Callback, KeyHandler};
6use crate::core::element::Element;
7use crate::core::event::{KeyCode, KeyEvent, KeyMods, MouseEvent};
8use crate::style::{Align, Length, Padding, Style, StyleSlot};
9use crate::widgets::Button;
10
11#[derive(Clone, Debug)]
13pub struct HyperlinkEvent {
14 pub label: Arc<str>,
16 pub href: Option<Arc<str>>,
18}
19
20#[derive(Clone)]
25pub struct Hyperlink {
26 label: Arc<str>,
27 href: Option<Arc<str>>,
28 style: Style,
29 hover_style: StyleSlot,
30 focus_style: StyleSlot,
31 disabled_style: Style,
32 visited_style: Option<Style>,
33 width: Length,
34 height: Length,
35 align: Align,
36 padding: Padding,
37 focusable: bool,
38 tab_stop: bool,
39 on_focus: Option<Callback<()>>,
40 on_blur: Option<Callback<()>>,
41 disabled: bool,
42 visited: bool,
43 on_activate: Option<Callback<HyperlinkEvent>>,
44 on_key: Option<KeyHandler>,
45}
46
47impl Hyperlink {
48 pub fn new(label: impl Into<Arc<str>>) -> Self {
50 Self {
51 label: label.into(),
52 href: None,
53 style: Style::new().underline(),
54 hover_style: StyleSlot::Extend(Style::new().underline()),
55 focus_style: StyleSlot::Extend(Style::new().underline().bold()),
56 disabled_style: Style::default(),
57 visited_style: None,
58 width: Length::Auto,
59 height: Length::Auto,
60 align: Align::Start,
61 padding: Padding::default(),
62 focusable: false,
63 tab_stop: true,
64 on_focus: None,
65 on_blur: None,
66 disabled: false,
67 visited: false,
68 on_activate: None,
69 on_key: None,
70 }
71 }
72
73 pub fn href(mut self, href: impl Into<Arc<str>>) -> Self {
75 self.href = Some(href.into());
76 self
77 }
78
79 pub fn style(mut self, style: Style) -> Self {
81 self.style = style;
82 self
83 }
84
85 pub fn hover_style(mut self, style: Style) -> Self {
87 self.hover_style = StyleSlot::Replace(style);
88 self
89 }
90
91 pub fn extend_hover_style(mut self, style: Style) -> Self {
93 self.hover_style = StyleSlot::Extend(style);
94 self
95 }
96
97 pub fn inherit_hover_style(mut self) -> Self {
99 self.hover_style = StyleSlot::Inherit;
100 self
101 }
102
103 pub fn hover_style_slot(mut self, slot: StyleSlot) -> Self {
105 self.hover_style = slot;
106 self
107 }
108
109 pub fn focus_style(mut self, style: Style) -> Self {
111 self.focus_style = StyleSlot::Replace(style);
112 self
113 }
114
115 pub fn extend_focus_style(mut self, style: Style) -> Self {
117 self.focus_style = StyleSlot::Extend(style);
118 self
119 }
120
121 pub fn inherit_focus_style(mut self) -> Self {
123 self.focus_style = StyleSlot::Inherit;
124 self
125 }
126
127 pub fn focus_style_slot(mut self, slot: StyleSlot) -> Self {
129 self.focus_style = slot;
130 self
131 }
132
133 pub fn disabled_style(mut self, style: Style) -> Self {
135 self.disabled_style = style;
136 self
137 }
138
139 pub fn visited_style(mut self, style: Style) -> Self {
141 self.visited_style = Some(style);
142 self
143 }
144
145 pub fn width(mut self, width: Length) -> Self {
147 self.width = width;
148 self
149 }
150
151 pub fn height(mut self, height: Length) -> Self {
153 self.height = height;
154 self
155 }
156
157 pub fn align(mut self, align: Align) -> Self {
159 self.align = align;
160 self
161 }
162
163 pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
165 self.padding = padding.into();
166 self
167 }
168
169 pub fn focusable(mut self, focusable: bool) -> Self {
171 self.focusable = focusable;
172 self
173 }
174
175 pub fn tab_stop(mut self, tab_stop: bool) -> Self {
177 self.tab_stop = tab_stop;
178 self
179 }
180
181 pub fn on_focus(mut self, cb: Callback<()>) -> Self {
183 self.on_focus = Some(cb);
184 self
185 }
186
187 pub fn on_blur(mut self, cb: Callback<()>) -> Self {
189 self.on_blur = Some(cb);
190 self
191 }
192
193 pub fn disabled(mut self, disabled: bool) -> Self {
195 self.disabled = disabled;
196 self
197 }
198
199 pub fn visited(mut self, visited: bool) -> Self {
201 self.visited = visited;
202 self
203 }
204
205 pub fn on_activate(mut self, cb: Callback<HyperlinkEvent>) -> Self {
209 self.on_activate = Some(cb);
210 self
211 }
212
213 pub fn on_key(mut self, cb: KeyHandler) -> Self {
217 self.on_key = Some(cb);
218 self
219 }
220}
221
222impl From<Hyperlink> for Element {
223 fn from(value: Hyperlink) -> Self {
224 let Hyperlink {
225 label,
226 href,
227 style,
228 hover_style,
229 focus_style,
230 disabled_style,
231 visited_style,
232 width,
233 height,
234 align,
235 padding,
236 focusable,
237 tab_stop,
238 on_focus,
239 on_blur,
240 disabled,
241 visited,
242 on_activate,
243 on_key,
244 } = value;
245
246 let style = apply_visited_style(style, visited, visited_style);
247
248 let mut button = Button::filled(label.clone())
249 .style(style)
250 .hover_style_slot(hover_style)
251 .focus_style_slot(focus_style)
252 .disabled_style(disabled_style)
253 .width(width)
254 .height(height)
255 .align(align)
256 .padding(padding)
257 .focusable(focusable && !disabled)
258 .tab_stop(tab_stop)
259 .disabled(disabled);
260
261 if let Some(cb) = on_focus {
262 button = button.on_focus(cb);
263 }
264 if let Some(cb) = on_blur {
265 button = button.on_blur(cb);
266 }
267
268 if let Some(cb) = on_activate.clone() {
269 let event_label = label.clone();
270 let event_href = href.clone();
271 button = button.on_click(Callback::new(move |_: MouseEvent| {
272 cb.emit(HyperlinkEvent {
273 label: event_label.clone(),
274 href: event_href.clone(),
275 });
276 }));
277 }
278
279 match (on_activate, on_key) {
280 (Some(cb), custom_key) => {
281 let event_label = label;
282 let event_href = href;
283 button = button.on_key(KeyHandler::new(move |key: KeyEvent| {
284 if custom_key.as_ref().is_some_and(|h| h.handle(key)) {
285 return true;
286 }
287 if is_activation_key(key) {
288 cb.emit(HyperlinkEvent {
289 label: event_label.clone(),
290 href: event_href.clone(),
291 });
292 return true;
293 }
294 false
295 }));
296 }
297 (None, Some(custom_key)) => {
298 button = button.on_key(custom_key);
299 }
300 (None, None) => {}
301 }
302
303 button.into()
304 }
305}
306
307fn is_activation_key(key: KeyEvent) -> bool {
308 if has_non_shift_modifiers(key.mods) {
309 return false;
310 }
311
312 matches!(key.code, KeyCode::Enter | KeyCode::Char(' '))
313}
314
315fn has_non_shift_modifiers(mods: KeyMods) -> bool {
316 mods.ctrl || mods.alt || mods.super_key
317}
318
319fn apply_visited_style(style: Style, visited: bool, visited_style: Option<Style>) -> Style {
320 if visited {
321 style.patch(visited_style.unwrap_or_default())
322 } else {
323 style
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::{apply_visited_style, is_activation_key};
330 use crate::core::event::{KeyCode, KeyEvent, KeyMods};
331 use crate::style::Style;
332
333 #[test]
334 fn enter_and_space_are_activation_keys() {
335 let enter = KeyEvent {
336 code: KeyCode::Enter,
337 mods: KeyMods::default(),
338 };
339 let space = KeyEvent {
340 code: KeyCode::Char(' '),
341 mods: KeyMods::default(),
342 };
343
344 assert!(is_activation_key(enter));
345 assert!(is_activation_key(space));
346 }
347
348 #[test]
349 fn ctrl_modified_key_is_not_activation() {
350 let key = KeyEvent {
351 code: KeyCode::Enter,
352 mods: KeyMods {
353 ctrl: true,
354 ..KeyMods::default()
355 },
356 };
357
358 assert!(!is_activation_key(key));
359 }
360
361 #[test]
362 fn visited_style_overlays_base_style() {
363 let base = Style::new().fg(crate::style::Color::Blue).underline();
364 let visited = Style::new().fg(crate::style::Color::Magenta);
365
366 let resolved = apply_visited_style(base, true, Some(visited));
367
368 assert_eq!(resolved.fg, Some(crate::style::Color::Magenta.into()));
369 assert_eq!(resolved.underline, Some(true));
370 }
371}