Skip to main content

agg_gui/widgets/
hyperlink.rs

1//! `Hyperlink` — a clickable label rendered in link style (blue, underlined).
2//!
3//! Unlike a full URL-opening widget, `Hyperlink` fires a plain `on_click`
4//! callback so callers can open URLs via whatever platform mechanism is
5//! available (`web_sys::window().open()` on WASM, `open::open()` on native).
6
7use std::sync::Arc;
8
9
10use crate::event::{Event, EventResult, MouseButton};
11use crate::geometry::{Rect, Size};
12use crate::draw_ctx::DrawCtx;
13use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
14use crate::text::Font;
15use crate::widget::Widget;
16
17// Colors are resolved from ctx.visuals() at paint time.
18
19/// A text label that looks like a hyperlink (blue, underlined) and fires a
20/// callback when clicked.
21pub struct Hyperlink {
22    bounds:   Rect,
23    children: Vec<Box<dyn Widget>>,
24    base:     WidgetBase,
25
26    text:      String,
27    font:      Arc<Font>,
28    font_size: f64,
29
30    hovered:  bool,
31    on_click: Option<Box<dyn FnMut()>>,
32
33    cache:    crate::widget::BackbufferCache,
34    last_sig: Option<(bool, u64, u64)>,  // (hovered, w_bits, h_bits)
35}
36
37impl Hyperlink {
38    pub fn new(text: impl Into<String>, font: Arc<Font>) -> Self {
39        Self {
40            bounds:   Rect::default(),
41            children: Vec::new(),
42            base:     WidgetBase::new(),
43            text:     text.into(),
44            font,
45            font_size: 14.0,
46            hovered:  false,
47            on_click: None,
48            cache:    crate::widget::BackbufferCache::default(),
49            last_sig: None,
50        }
51    }
52
53    pub fn with_font_size(mut self, size: f64) -> Self { self.font_size = size; self }
54    pub fn on_click(mut self, cb: impl FnMut() + 'static) -> Self {
55        self.on_click = Some(Box::new(cb)); self
56    }
57
58    pub fn with_margin(mut self, m: Insets)    -> Self { self.base.margin   = m; self }
59    pub fn with_h_anchor(mut self, h: HAnchor) -> Self { self.base.h_anchor = h; self }
60    pub fn with_v_anchor(mut self, v: VAnchor) -> Self { self.base.v_anchor = v; self }
61    pub fn with_min_size(mut self, s: Size)    -> Self { self.base.min_size = s; self }
62    pub fn with_max_size(mut self, s: Size)    -> Self { self.base.max_size = s; self }
63}
64
65impl Widget for Hyperlink {
66    fn type_name(&self) -> &'static str { "Hyperlink" }
67    fn bounds(&self) -> Rect { self.bounds }
68    fn set_bounds(&mut self, b: Rect) { self.bounds = b; }
69    fn children(&self) -> &[Box<dyn Widget>] { &self.children }
70    fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> { &mut self.children }
71
72    fn margin(&self)   -> Insets  { self.base.margin }
73    fn h_anchor(&self) -> HAnchor { self.base.h_anchor }
74    fn v_anchor(&self) -> VAnchor { self.base.v_anchor }
75    fn min_size(&self) -> Size    { self.base.min_size }
76    fn max_size(&self) -> Size    { self.base.max_size }
77
78    fn is_focusable(&self) -> bool { true }
79
80    fn backbuffer_cache_mut(&mut self) -> Option<&mut crate::widget::BackbufferCache> {
81        Some(&mut self.cache)
82    }
83
84    fn backbuffer_mode(&self) -> crate::widget::BackbufferMode {
85        if crate::font_settings::lcd_enabled() {
86            crate::widget::BackbufferMode::LcdCoverage
87        } else {
88            crate::widget::BackbufferMode::Rgba
89        }
90    }
91
92    fn layout(&mut self, available: Size) -> Size {
93        let sig = (self.hovered, self.bounds.width.to_bits(), self.bounds.height.to_bits());
94        if self.last_sig != Some(sig) {
95            self.last_sig = Some(sig);
96            self.cache.invalidate();
97        }
98        let h = self.font_size * 1.5;
99        Size::new(available.width, h)
100    }
101
102    fn paint(&mut self, ctx: &mut dyn DrawCtx) {
103        let v = ctx.visuals();
104        let color = if self.hovered { v.text_link_hovered } else { v.text_link };
105        ctx.set_font(Arc::clone(&self.font));
106        ctx.set_font_size(self.font_size);
107        ctx.set_fill_color(color);
108
109        let h = self.bounds.height;
110        let m = ctx.measure_text(&self.text).unwrap_or_default();
111        let ty = h * 0.5 - (m.ascent - m.descent) * 0.5;
112        ctx.fill_text(&self.text, 0.0, ty);
113
114        // Underline — drawn at the text baseline.
115        let uw = m.width;
116        let uy = ty - m.descent - 1.0; // 1 px below baseline
117        ctx.set_stroke_color(color);
118        ctx.set_line_width(1.0);
119        ctx.begin_path();
120        ctx.move_to(0.0, uy);
121        ctx.line_to(uw, uy);
122        ctx.stroke();
123    }
124
125    fn on_event(&mut self, event: &Event) -> EventResult {
126        match event {
127            Event::MouseMove { pos } => {
128                let was = self.hovered;
129                self.hovered = self.hit_test(*pos);
130                if was != self.hovered { crate::animation::request_tick(); }
131                EventResult::Ignored
132            }
133            Event::MouseDown { button: MouseButton::Left, .. } => EventResult::Consumed,
134            Event::MouseUp   { button: MouseButton::Left, pos, .. } => {
135                if self.hit_test(*pos) {
136                    if let Some(cb) = self.on_click.as_mut() { cb(); }
137                    // Click handler typically mutates app state.
138                    crate::animation::request_tick();
139                }
140                EventResult::Consumed
141            }
142            _ => EventResult::Ignored,
143        }
144    }
145}