1use std::cell::RefCell;
5use std::fmt;
6use std::rc::Rc;
7
8use ravenrs_core::{Color, CursorIcon, Insets, Role};
9use ravenrs_layout::{Align, Axis, LayoutStyle};
10use ravenrs_style::{Semantic, TextStyle};
11
12use crate::context::{BuildCx, NodeId, Paint, QuadPaint, Semantics};
13use crate::{IconName, View, util};
14
15pub trait LinkOpener {
18 fn open(&self, url: &str);
20}
21
22#[derive(Debug, Default)]
24struct Recorder;
25
26impl LinkOpener for Recorder {
27 fn open(&self, url: &str) {
28 tracing::info!(url, "no link opener installed");
29 }
30}
31
32#[derive(Clone, Default)]
44#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
45pub struct Links {
46 opener: Rc<RefCell<Option<Rc<dyn LinkOpener>>>>,
47 anchor: Rc<RefCell<Option<String>>>,
48 opened: Rc<RefCell<Vec<String>>>,
49}
50
51#[cfg_attr(feature = "live", ravenrs_live::expose)]
52impl Links {
53 pub fn open(&self, url: &str) {
55 if let Some(name) = url.strip_prefix('#') {
56 *self.anchor.borrow_mut() = Some(name.to_owned());
57 return;
58 }
59 self.opened.borrow_mut().push(url.to_owned());
60 let opener = self.opener.borrow().clone().unwrap_or_else(|| Rc::new(Recorder));
61 opener.open(url);
62 }
63
64 pub fn opened(&self) -> Vec<String> {
66 self.opened.borrow().clone()
67 }
68
69 pub(crate) fn set_opener(&self, opener: impl LinkOpener + 'static) {
70 *self.opener.borrow_mut() = Some(Rc::new(opener));
71 }
72
73 pub(crate) fn take_anchor(&self) -> Option<String> {
75 self.anchor.borrow_mut().take()
76 }
77}
78
79impl fmt::Debug for Links {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 f.debug_struct("Links").field("opened", &self.opened.borrow().len()).finish_non_exhaustive()
82 }
83}
84
85#[derive(Debug)]
88#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
89pub struct Link {
90 label: String,
91 url: String,
92 style: TextStyle,
93 icon: Option<IconName>,
94 muted: bool,
95}
96
97#[cfg_attr(feature = "live", ravenrs_live::expose)]
98impl Link {
99 pub fn new(label: impl Into<String>, url: impl Into<String>) -> Self {
101 Self { label: label.into(), url: url.into(), style: TextStyle::Body, icon: None, muted: false }
102 }
103
104 pub fn style(mut self, style: TextStyle) -> Self {
106 self.style = style;
107 self
108 }
109
110 pub fn icon(mut self, icon: IconName) -> Self {
112 self.icon = Some(icon);
113 self
114 }
115
116 pub fn muted(mut self) -> Self {
118 self.muted = true;
119 self
120 }
121}
122
123impl View for Link {
124 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
125 let theme = *cx.theme();
126 let lit = cx.is_hovered() || cx.is_focus_visible();
127 let ink = theme.color(Semantic::Ink);
128 let rest = if self.muted {
129 theme.color(Semantic::InkMuted)
130 } else {
131 theme.color(Semantic::Accent).lerp(ink, 0.4)
133 };
134 let color = if lit { ink } else { rest };
135 let mut parts =
136 vec![util::label(cx, &self.label, self.style, Some(ravenrs_style::Weight::SEMIBOLD), color)];
137 if let Some(icon) = self.icon {
138 parts.push(util::icon(cx, icon, 16.0, color));
139 }
140 let fill = if lit { ink.with_alpha(0.06) } else { Color::TRANSPARENT };
141 let node = cx.container(
142 LayoutStyle {
143 padding: Insets::symmetric(4.0, 6.0),
144 spacing: 6.0,
145 align: Align::Center,
146 align_self: Some(Align::Start),
147 ..LayoutStyle::stack(Axis::Horizontal)
148 },
149 &parts,
150 Paint::Quad(QuadPaint { corner_radius: theme.radii.sm, ..QuadPaint::new(fill) }),
151 );
152 let (links, url) = (cx.links(), self.url.clone());
153 cx.on_click(node, Rc::new(move || links.open(&url)));
154 cx.semantics(node, Semantics::new(Role::Link).name(self.label.clone()));
155 cx.focusable(node, theme.radii.sm);
156 cx.cursor(node, CursorIcon::Pointer);
157 node
158 }
159}