1use jag_draw::{ColorLinPremul, FontStyle, Hyperlink, Rect};
4use jag_surface::Canvas;
5
6use crate::event::{
7 ElementState, EventHandler, EventResult, KeyCode, KeyboardEvent, MouseButton, MouseClickEvent,
8 MouseMoveEvent, ScrollEvent,
9};
10use crate::focus::FocusId;
11
12use super::Element;
13
14pub struct Link {
16 pub text: String,
18 pub pos: [f32; 2],
20 pub size: f32,
22 pub color: ColorLinPremul,
24 pub url: String,
26 pub weight: f32,
28 pub measured_width: Option<f32>,
30 pub underline: bool,
32 pub underline_color: Option<ColorLinPremul>,
34 pub font_family: Option<String>,
36 pub font_style: FontStyle,
38 pub focused: bool,
40 pub hovered: bool,
42 pub focus_id: FocusId,
44}
45
46impl Link {
47 pub fn new(text: impl Into<String>, url: impl Into<String>, pos: [f32; 2], size: f32) -> Self {
49 Self {
50 text: text.into(),
51 pos,
52 size,
53 color: ColorLinPremul::from_srgba_u8([0x00, 0x7a, 0xff, 0xff]),
54 url: url.into(),
55 weight: 400.0,
56 measured_width: None,
57 underline: true,
58 underline_color: None,
59 font_family: None,
60 font_style: FontStyle::Normal,
61 focused: false,
62 hovered: false,
63 focus_id: FocusId(0),
64 }
65 }
66
67 pub fn with_color(mut self, color: ColorLinPremul) -> Self {
69 self.color = color;
70 self
71 }
72
73 pub fn with_weight(mut self, weight: f32) -> Self {
75 self.weight = weight;
76 self
77 }
78
79 pub fn with_measured_width(mut self, measured_width: f32) -> Self {
81 self.measured_width = Some(measured_width.max(0.0));
82 self
83 }
84
85 pub fn with_underline(mut self, underline: bool) -> Self {
87 self.underline = underline;
88 self
89 }
90
91 pub fn get_url(&self) -> &str {
93 &self.url
94 }
95
96 pub fn set_url(&mut self, url: String) {
98 self.url = url;
99 }
100
101 fn get_bounds(&self) -> (f32, f32, f32, f32) {
103 let char_width = self.size * 0.5;
104 let text_width = self.text.len() as f32 * char_width;
105 let text_height = self.size * 1.2;
106 (
107 self.pos[0],
108 self.pos[1] - self.size * 0.8,
109 text_width,
110 text_height,
111 )
112 }
113
114 pub fn hit_test(&self, x: f32, y: f32) -> bool {
116 let (bx, by, bw, bh) = self.get_bounds();
117 x >= bx && x <= bx + bw && y >= by && y <= by + bh
118 }
119}
120
121impl Default for Link {
122 fn default() -> Self {
123 Self::new("Link", "https://example.com", [0.0, 0.0], 16.0)
124 }
125}
126
127impl Element for Link {
132 fn rect(&self) -> Rect {
133 let (bx, by, bw, bh) = self.get_bounds();
134 Rect {
135 x: bx,
136 y: by,
137 w: bw,
138 h: bh,
139 }
140 }
141
142 fn set_rect(&mut self, rect: Rect) {
143 self.pos = [rect.x, rect.y + rect.h * 0.8];
144 }
145
146 fn render(&self, canvas: &mut Canvas, z: i32) {
147 let hyperlink = Hyperlink {
148 text: self.text.clone(),
149 pos: self.pos,
150 size: self.size,
151 color: self.color,
152 url: self.url.clone(),
153 weight: self.weight,
154 measured_width: self.measured_width,
155 underline: self.underline,
156 underline_color: self.underline_color,
157 family: self.font_family.clone(),
158 style: self.font_style,
159 };
160 canvas.draw_hyperlink(hyperlink, z);
161 }
162
163 fn focus_id(&self) -> Option<FocusId> {
164 Some(self.focus_id)
165 }
166}
167
168impl EventHandler for Link {
173 fn handle_mouse_click(&mut self, event: &MouseClickEvent) -> EventResult {
174 if event.button != MouseButton::Left || event.state != ElementState::Pressed {
175 return EventResult::Ignored;
176 }
177 if self.hit_test(event.x, event.y) {
178 EventResult::Handled
179 } else {
180 EventResult::Ignored
181 }
182 }
183
184 fn handle_keyboard(&mut self, event: &KeyboardEvent) -> EventResult {
185 if event.state != ElementState::Pressed || !self.focused {
186 return EventResult::Ignored;
187 }
188 match event.key {
189 KeyCode::Space | KeyCode::Enter => EventResult::Handled,
190 _ => EventResult::Ignored,
191 }
192 }
193
194 fn handle_mouse_move(&mut self, event: &MouseMoveEvent) -> EventResult {
195 let was_hovered = self.hovered;
196 self.hovered = self.hit_test(event.x, event.y);
197 if was_hovered != self.hovered {
198 EventResult::Handled
199 } else {
200 EventResult::Ignored
201 }
202 }
203
204 fn handle_scroll(&mut self, _event: &ScrollEvent) -> EventResult {
205 EventResult::Ignored
206 }
207
208 fn is_focused(&self) -> bool {
209 self.focused
210 }
211
212 fn set_focused(&mut self, focused: bool) {
213 self.focused = focused;
214 }
215
216 fn contains_point(&self, x: f32, y: f32) -> bool {
217 self.hit_test(x, y)
218 }
219}
220
221#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn link_new_defaults() {
231 let link = Link::new("Click", "https://example.com", [10.0, 20.0], 14.0);
232 assert_eq!(link.text, "Click");
233 assert_eq!(link.url, "https://example.com");
234 assert!(link.underline);
235 assert!(!link.focused);
236 assert!(!link.hovered);
237 }
238
239 #[test]
240 fn link_hit_test() {
241 let link = Link::new("Hello", "https://example.com", [10.0, 20.0], 14.0);
242 assert!(link.hit_test(15.0, 15.0));
244 assert!(!link.hit_test(0.0, 0.0));
245 }
246
247 #[test]
248 fn link_builder_methods() {
249 let link = Link::new("Test", "http://test.com", [0.0, 0.0], 16.0)
250 .with_weight(700.0)
251 .with_underline(false)
252 .with_measured_width(100.0);
253 assert_eq!(link.weight, 700.0);
254 assert!(!link.underline);
255 assert_eq!(link.measured_width, Some(100.0));
256 }
257
258 #[test]
259 fn link_get_set_url() {
260 let mut link = Link::default();
261 assert_eq!(link.get_url(), "https://example.com");
262 link.set_url("https://other.com".to_string());
263 assert_eq!(link.get_url(), "https://other.com");
264 }
265
266 #[test]
267 fn link_hover_state() {
268 let mut link = Link::new("Hover", "https://example.com", [10.0, 20.0], 14.0);
269 assert!(!link.hovered);
270 let evt = MouseMoveEvent { x: 15.0, y: 15.0 };
271 let result = link.handle_mouse_move(&evt);
272 assert_eq!(result, EventResult::Handled);
273 assert!(link.hovered);
274 }
275}