use gpui::prelude::FluentBuilder as _; use gpui::{ App, BoxShadow, ClickEvent, ElementId, InteractiveElement as _, IntoElement, ParentElement as _, RenderOnce, Role, SharedString, StatefulInteractiveElement as _, Styled as _, Window, px, }; use gpui_native_theme::{ActiveTheme as _, Size}; use crate::common::{Sizable, h_flex}; /// The click handler. When set it replaces the default `href` opening. type ClickHandler = Box; /// Link component. #[derive(IntoElement)] pub struct Link { id: ElementId, label: Option, href: Option, size: Size, disabled: bool, a11y_label: Option, on_click: Option, } impl Link { /// Creates a link without a label or target. pub fn new(id: I) -> Self where I: Into, { Self { id: id.into(), label: None, href: None, size: Size::default(), disabled: false, a11y_label: None, on_click: None, } } /// Sets the visible label. pub fn label(mut self, label: L) -> Self where L: Into, { self.label = Some(label.into()); self } /// Sets the URL the link opens through the platform. pub fn href(mut self, href: U) -> Self where U: Into, { self.href = Some(href.into()); self } /// Renders the link inert, unfocusable, and dimmed; disabled always wins. pub fn disabled(mut self, disabled: bool) -> Self { self.disabled = disabled; self } /// Overrides the accessibility label, which defaults to the visible label. pub fn accessibility_label(mut self, label: L) -> Self where L: Into, { self.a11y_label = Some(label.into()); self } /// Replaces the default `href` opening with this handler. pub fn on_click(mut self, handler: F) -> Self where F: Fn(&ClickEvent, &mut Window, &mut App) + 'static, { self.on_click = Some(Box::new(handler)); self } } impl Sizable for Link { fn apply_size(&mut self, size: Size) { self.size = size; } } impl RenderOnce for Link { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { let theme = cx.theme(); let colors = theme.colors; let ramps = theme.ramps; let metrics = theme.sizes.resolve(self.size); let on_click = self.on_click; let href = self.href; let a11y_label = self.a11y_label.or_else(|| self.label.clone()); h_flex() .id(self.id) .text_size(metrics.text_size) .text_color(colors.accent) .underline() .role(Role::Link) .when_some(a11y_label, |link, label| link.aria_label(label)) .when(self.disabled, |link| link.opacity(0.5)) .when(!self.disabled, |link| { link.cursor_pointer() .focusable() .tab_stop(true) .hover(move |style| style.text_color(ramps.primary.hover)) .focus_visible(|style| { style.shadow(vec![ BoxShadow::new(px(0.), px(0.), colors.ring).spread_radius(px(3.)), ]) }) .on_click(move |event, window, cx| match on_click.as_ref() { Some(on_click) => on_click(event, window, cx), None => { if let Some(href) = href.as_ref() { cx.open_url(href); } } }) }) .when_some(self.label, |link, label| link.child(label)) } }