1use super::*;
2use crate::bindings::ui;
3use crate::event::{PointerButton, PointerType};
4use crate::ffi::{CursorStyle, HandleValue, SemanticRole};
5use crate::navigation;
6use crate::node::{text, NodeHandle, NodeRef, WeakFlexBox};
7use crate::platform;
8use crate::theme::{current_theme, subscribe};
9use crate::{focus_adorner, focus_visibility};
10use std::cell::{Cell, RefCell};
11use std::rc::Rc;
12
13thread_local! {
14 static ACTIVE_PREVIEW_OWNER: Cell<u64> = const { Cell::new(HandleValue::Invalid as u64) };
15}
16
17fn is_middle_mouse_button(event: &PointerEventArgs) -> bool {
18 event.pointer_type == PointerType::Mouse && event.button == PointerButton::Auxiliary
19}
20
21fn is_activation_pointer(event: &PointerEventArgs) -> bool {
22 event.button == PointerButton::Primary
23 || is_middle_mouse_button(event)
24 || event.pointer_type == PointerType::Touch
25 || event.pointer_type == PointerType::Pen
26}
27
28#[derive(Clone, Debug, PartialEq, Eq)]
29pub struct NavigateEventArgs {
30 pub path: String,
31}
32
33type NavigateCallback = Rc<dyn Fn(NavigateEventArgs)>;
34
35#[derive(Clone)]
36pub struct NavLink {
37 root: FlexBox,
38 label: TextNode,
39 href: Rc<RefCell<String>>,
40 open_in_new_tab: Rc<Cell<bool>>,
41 navigate: Rc<RefCell<Option<NavigateCallback>>>,
42 hovered: Rc<Cell<bool>>,
43 focused: Rc<Cell<bool>>,
44 pointer_pressed: Rc<Cell<bool>>,
45 pointer_pressed_open_in_new_tab: Rc<Cell<bool>>,
46 enter_pressed: Rc<Cell<bool>>,
47 enter_pressed_open_in_new_tab: Rc<Cell<bool>>,
48 preview_pinned_for_context_menu: Rc<Cell<bool>>,
49 text_color_override: Rc<Cell<Option<u32>>>,
50}
51
52impl NavLink {
53 pub fn new(href: impl Into<String>) -> Self {
54 let href = href.into();
55 Self::with_label(href.clone(), href)
56 }
57
58 pub fn with_label(href: impl Into<String>, label: impl Into<String>) -> Self {
59 let href = href.into();
60 let label = label.into();
61 let root = row();
62 let label_node = text(&label);
63 let theme = current_theme();
64 label_node
65 .font_family(theme.fonts.body_family.clone())
66 .font_size(15.0)
67 .text_color(theme.colors.accent)
68 .selectable(false)
69 .cursor(CursorStyle::Pointer);
70 root.justify_content(JustifyContent::Start)
71 .align_items(AlignItems::Center)
72 .interactive(true)
73 .focusable(true, 0)
74 .cursor(CursorStyle::Pointer)
75 .semantic_role(SemanticRole::Link)
76 .semantic_label(label)
77 .reflect_semantic_disabled_from_enabled()
78 .child(&label_node);
79 root.retained_node_ref()
80 .set_link_url_for_routing(Some(href.clone()));
81
82 let link = Self {
83 root,
84 label: label_node,
85 href: Rc::new(RefCell::new(href)),
86 open_in_new_tab: Rc::new(Cell::new(false)),
87 navigate: Rc::new(RefCell::new(None)),
88 hovered: Rc::new(Cell::new(false)),
89 focused: Rc::new(Cell::new(false)),
90 pointer_pressed: Rc::new(Cell::new(false)),
91 pointer_pressed_open_in_new_tab: Rc::new(Cell::new(false)),
92 enter_pressed: Rc::new(Cell::new(false)),
93 enter_pressed_open_in_new_tab: Rc::new(Cell::new(false)),
94 preview_pinned_for_context_menu: Rc::new(Cell::new(false)),
95 text_color_override: Rc::new(Cell::new(None)),
96 };
97 let node_ref = link.root.retained_node_ref();
98 let pin_target = link.event_target();
99 let release_target = link.event_target();
100 node_ref.set_link_preview_handlers_for_routing(
101 Some(Rc::new(move || {
102 pin_target.pin_preview_for_context_menu();
103 })),
104 Some(Rc::new(move || {
105 release_target.release_preview_for_context_menu();
106 })),
107 );
108 link.install_subscriptions();
109 link.bind_events();
110 link.sync_visual_state();
111 link.sync_focus_chrome();
112 link
113 }
114
115 fn bind_events(&self) {
116 let target = self.event_target();
117 self.root.on_pointer_enter(move |_event| {
118 target.hovered.set(true);
119 target.sync_visual_state();
120 target.show_preview();
121 });
122 let target = self.event_target();
123 self.root.on_pointer_leave(move |_event| {
124 target.hovered.set(false);
125 target.pointer_pressed.set(false);
126 target.pointer_pressed_open_in_new_tab.set(false);
127 target.sync_visual_state();
128 if !target.focused.get() && !target.preview_pinned_for_context_menu.get() {
129 target.hide_preview();
130 }
131 });
132 let target = self.event_target();
133 self.root.on_pointer_down(move |event| {
134 if !is_activation_pointer(event) {
135 target.pointer_pressed.set(false);
136 target.pointer_pressed_open_in_new_tab.set(false);
137 return;
138 }
139 target.pointer_pressed.set(true);
140 target.pointer_pressed_open_in_new_tab.set(
141 is_middle_mouse_button(event) || target.should_open_in_new_tab(event.modifiers),
142 );
143 event.handled = true;
144 });
145 let target = self.event_target();
146 self.root.on_pointer_up(move |event| {
147 if target.pointer_pressed.replace(false) && is_activation_pointer(event) {
148 let open_in_new_tab = target.pointer_pressed_open_in_new_tab.get()
149 || is_middle_mouse_button(event)
150 || target.should_open_in_new_tab(event.modifiers);
151 target.activate(open_in_new_tab);
152 event.handled = true;
153 }
154 target.pointer_pressed_open_in_new_tab.set(false);
155 });
156 let target = self.event_target();
157 self.root.on_key_down(move |event| {
158 if !target.should_handle_enter_key(event.modifiers) || event.key != "Enter" {
159 return;
160 }
161 if target.enter_pressed.get() {
162 event.handled = true;
163 return;
164 }
165 target.enter_pressed.set(true);
166 target
167 .enter_pressed_open_in_new_tab
168 .set(target.should_open_in_new_tab(event.modifiers));
169 event.handled = true;
170 });
171 let target = self.event_target();
172 self.root.on_key_up(move |event| {
173 if !target.should_handle_enter_key(event.modifiers) || event.key != "Enter" {
174 return;
175 }
176 if target.enter_pressed.replace(false) {
177 let open_in_new_tab = target.enter_pressed_open_in_new_tab.get()
178 || target.should_open_in_new_tab(event.modifiers);
179 target.activate(open_in_new_tab);
180 event.handled = true;
181 }
182 target.enter_pressed_open_in_new_tab.set(false);
183 });
184 let target = self.event_target();
185 self.root.on_focus_changed(move |event| {
186 target.focused.set(event.focused);
187 if !event.focused {
188 target.enter_pressed.set(false);
189 target.enter_pressed_open_in_new_tab.set(false);
190 }
191 target.sync_focus_chrome();
192 if event.focused {
193 target.show_preview();
194 } else if !target.hovered.get() && !target.preview_pinned_for_context_menu.get() {
195 target.hide_preview();
196 }
197 });
198 }
199
200 fn install_subscriptions(&self) {
201 let target = self.event_target();
202 let theme_guard = subscribe(move |_theme| {
203 target.sync_visual_state();
204 target.sync_focus_chrome();
205 });
206 self.root
207 .retained_node_ref()
208 .retain_attachment(Rc::new(theme_guard));
209 let target = self.event_target();
210 let focus_guard = focus_visibility::subscribe(move |_visible| {
211 target.sync_focus_chrome();
212 });
213 self.root
214 .retained_node_ref()
215 .retain_attachment(Rc::new(focus_guard));
216 }
217
218 fn event_target(&self) -> NavLinkEventTarget {
219 NavLinkEventTarget {
220 weak_root: self.root.downgrade(),
221 label: self.label.clone(),
222 href: self.href.clone(),
223 open_in_new_tab: self.open_in_new_tab.clone(),
224 navigate: self.navigate.clone(),
225 hovered: self.hovered.clone(),
226 focused: self.focused.clone(),
227 pointer_pressed: self.pointer_pressed.clone(),
228 pointer_pressed_open_in_new_tab: self.pointer_pressed_open_in_new_tab.clone(),
229 enter_pressed: self.enter_pressed.clone(),
230 enter_pressed_open_in_new_tab: self.enter_pressed_open_in_new_tab.clone(),
231 preview_pinned_for_context_menu: self.preview_pinned_for_context_menu.clone(),
232 text_color_override: self.text_color_override.clone(),
233 }
234 }
235
236 fn set_explicit_font_family(&self, family: crate::FontFamily) {
237 self.label.font_family(family);
238 }
239
240 fn set_explicit_font_size(&self, size: f32) {
241 self.label.font_size(size);
242 }
243
244 fn set_explicit_text_color(&self, color: u32) {
245 self.text_color_override.set(Some(color));
246 self.label.text_color(color);
247 self.sync_visual_state();
248 }
249
250 pub fn href(&self) -> String {
251 self.href.borrow().clone()
252 }
253
254 pub fn href_to(&self, href: impl Into<String>) -> &Self {
255 let href = href.into();
256 *self.href.borrow_mut() = href.clone();
257 self.root
258 .retained_node_ref()
259 .set_link_url_for_routing(Some(href));
260 if self.focused.get() || self.hovered.get() || self.preview_pinned_for_context_menu.get() {
261 self.event_target().show_preview();
262 }
263 self
264 }
265
266 pub fn text(&self, value: impl Into<String>) -> &Self {
267 let value = value.into();
268 self.label.text(value.clone());
269 self.root.semantic_label(value);
270 self
271 }
272
273 pub fn label_node(&self) -> TextNode {
274 self.label.clone()
275 }
276
277 pub fn open_in_new_tab(&self, open: bool) -> &Self {
278 self.open_in_new_tab.set(open);
279 self
280 }
281
282 pub fn on_navigate(&self, handler: impl Fn(NavigateEventArgs) + 'static) -> &Self {
283 *self.navigate.borrow_mut() = Some(Rc::new(handler));
284 self
285 }
286
287 fn sync_visual_state(&self) {
288 self.event_target().sync_visual_state();
289 }
290
291 fn sync_focus_chrome(&self) {
292 self.event_target().sync_focus_chrome();
293 }
294}
295
296impl Node for NavLink {
297 fn retained_node_ref(&self) -> NodeRef {
298 self.root.retained_node_ref()
299 }
300
301 fn build_self(&self) {
302 self.root.build_self();
303 self.sync_visual_state();
304 }
305}
306
307impl HasFlexBoxRoot for NavLink {
308 fn flex_box_root(&self) -> &FlexBox {
309 &self.root
310 }
311}
312
313impl ThemeBindable for NavLink {
314 fn theme_binding_node(&self) -> NodeRef {
315 self.root.retained_node_ref()
316 }
317
318 fn weak_theme_target(&self) -> Box<dyn Fn() -> Option<Self>> {
319 let target = self.event_target();
320 Box::new(move || target.upgrade())
321 }
322}
323
324impl LabeledControlTextStyle for NavLink {
325 fn set_label_font_family(&self, family: crate::FontFamily) {
326 self.set_explicit_font_family(family);
327 }
328
329 fn set_label_font_size(&self, size: f32) {
330 self.set_explicit_font_size(size);
331 }
332
333 fn set_label_text_color(&self, color: u32) {
334 self.set_explicit_text_color(color);
335 }
336}
337
338#[derive(Clone)]
339struct NavLinkEventTarget {
340 weak_root: WeakFlexBox,
341 label: TextNode,
342 href: Rc<RefCell<String>>,
343 open_in_new_tab: Rc<Cell<bool>>,
344 navigate: Rc<RefCell<Option<NavigateCallback>>>,
345 hovered: Rc<Cell<bool>>,
346 focused: Rc<Cell<bool>>,
347 pointer_pressed: Rc<Cell<bool>>,
348 pointer_pressed_open_in_new_tab: Rc<Cell<bool>>,
349 enter_pressed: Rc<Cell<bool>>,
350 enter_pressed_open_in_new_tab: Rc<Cell<bool>>,
351 preview_pinned_for_context_menu: Rc<Cell<bool>>,
352 text_color_override: Rc<Cell<Option<u32>>>,
353}
354
355impl NavLinkEventTarget {
356 fn upgrade(&self) -> Option<NavLink> {
357 Some(NavLink {
358 root: self.weak_root.upgrade()?,
359 label: self.label.clone(),
360 href: self.href.clone(),
361 open_in_new_tab: self.open_in_new_tab.clone(),
362 navigate: self.navigate.clone(),
363 hovered: self.hovered.clone(),
364 focused: self.focused.clone(),
365 pointer_pressed: self.pointer_pressed.clone(),
366 pointer_pressed_open_in_new_tab: self.pointer_pressed_open_in_new_tab.clone(),
367 enter_pressed: self.enter_pressed.clone(),
368 enter_pressed_open_in_new_tab: self.enter_pressed_open_in_new_tab.clone(),
369 preview_pinned_for_context_menu: self.preview_pinned_for_context_menu.clone(),
370 text_color_override: self.text_color_override.clone(),
371 })
372 }
373
374 fn is_enabled(&self) -> bool {
375 self.weak_root
376 .upgrade()
377 .map(|root| root.retained_node_ref().is_enabled_for_routing())
378 .unwrap_or(false)
379 }
380
381 fn activate(&self, open_in_new_tab: bool) {
382 let href = self.href.borrow().clone();
383 navigation::navigate_to(&href, open_in_new_tab);
384 if let Some(callback) = self.navigate.borrow().clone() {
385 callback(NavigateEventArgs { path: href });
386 }
387 }
388
389 fn should_open_in_new_tab(&self, modifiers: u32) -> bool {
390 self.open_in_new_tab.get() || platform::has_primary_shortcut_modifier(modifiers)
391 }
392
393 fn should_handle_enter_key(&self, modifiers: u32) -> bool {
394 self.is_enabled() && (modifiers == 0 || platform::has_primary_shortcut_modifier(modifiers))
395 }
396
397 fn show_preview(&self) {
398 let Some(root) = self.weak_root.upgrade() else {
399 return;
400 };
401 let handle = root.handle().raw();
402 if handle == HandleValue::Invalid as u64 {
403 return;
404 }
405 let href = self.href.borrow().clone();
406 let bytes = href.as_bytes();
407 unsafe {
408 crate::ffi::fui_show_url_preview(
409 if bytes.is_empty() {
410 0
411 } else {
412 bytes.as_ptr() as usize
413 },
414 bytes.len() as u32,
415 )
416 };
417 ACTIVE_PREVIEW_OWNER.with(|owner| owner.set(handle));
418 }
419
420 fn hide_preview(&self) {
421 let Some(root) = self.weak_root.upgrade() else {
422 return;
423 };
424 let handle = root.handle().raw();
425 if !ACTIVE_PREVIEW_OWNER.with(|owner| owner.get() == handle) {
426 return;
427 }
428 ACTIVE_PREVIEW_OWNER.with(|owner| owner.set(HandleValue::Invalid as u64));
429 unsafe { crate::ffi::fui_hide_url_preview() };
430 }
431
432 fn pin_preview_for_context_menu(&self) {
433 self.preview_pinned_for_context_menu.set(true);
434 self.show_preview();
435 }
436
437 fn release_preview_for_context_menu(&self) {
438 if !self.preview_pinned_for_context_menu.replace(false) {
439 return;
440 }
441 if !self.hovered.get() && !self.focused.get() {
442 self.hide_preview();
443 }
444 }
445
446 fn sync_visual_state(&self) {
447 let theme = current_theme();
448 let color = if self.hovered.get() {
449 theme.colors.accent_hovered
450 } else {
451 self.text_color_override
452 .get()
453 .unwrap_or(theme.colors.accent)
454 };
455 if self.label.handle() != NodeHandle::INVALID {
456 ui::set_text_color(self.label.handle().raw(), color);
457 }
458 }
459
460 fn sync_focus_chrome(&self) {
461 let Some(root) = self.weak_root.upgrade() else {
462 return;
463 };
464 if self.focused.get() && self.is_enabled() && focus_visibility::keyboard_focus_visible() {
465 focus_adorner::show_standard(&root, current_theme().spacing.sm);
466 return;
467 }
468 focus_adorner::hide_owner(&root);
469 }
470}