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