use std::ops::{Deref, DerefMut};
use blinc_core::Color;
use blinc_theme::{ColorToken, ThemeState};
use crate::div::{div, Div, ElementBuilder};
use crate::element::RenderProps;
use crate::text::text;
use crate::tree::{LayoutNodeId, LayoutTree};
pub fn open_url(url: &str) {
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
{
if let Err(e) = open::that(url) {
tracing::warn!("Failed to open URL '{}': {}", url, e);
}
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{
tracing::warn!("URL opening not supported on this platform: {}", url);
}
}
#[derive(Clone, Debug)]
pub struct LinkConfig {
pub text_color: Color,
pub hover_color: Color,
pub font_size: f32,
pub underline: bool,
pub underline_on_hover_only: bool,
}
impl Default for LinkConfig {
fn default() -> Self {
let theme = ThemeState::get();
Self {
text_color: theme.color(ColorToken::TextLink),
hover_color: theme.color(ColorToken::PrimaryHover),
font_size: 16.0,
underline: true,
underline_on_hover_only: false,
}
}
}
pub struct Link {
inner: Div,
url: String,
css_element_id: Option<String>,
css_classes: Vec<String>,
}
impl Link {
pub fn new(label: impl Into<String>, url: impl Into<String>) -> Self {
let config = LinkConfig::default();
let label = label.into();
let url = url.into();
let mut text_element = text(&label)
.size(config.font_size)
.color(config.text_color)
.no_cursor();
if config.underline && !config.underline_on_hover_only {
text_element = text_element.underline();
}
let url_for_click = url.clone();
let inner = div()
.child(text_element)
.cursor_pointer()
.on_click(move |_ctx| {
open_url(&url_for_click);
});
Self {
inner,
url,
css_element_id: None,
css_classes: Vec::new(),
}
}
pub fn on_click<F>(self, handler: F) -> Self
where
F: Fn(&str, &crate::event_handler::EventContext) + Send + Sync + 'static,
{
let label = self.extract_label();
let url = self.url;
let config = LinkConfig::default();
let mut text_element = text(&label)
.size(config.font_size)
.color(config.text_color)
.no_cursor();
if config.underline && !config.underline_on_hover_only {
text_element = text_element.underline();
}
let url_for_click = url.clone();
let inner = div()
.child(text_element)
.cursor_pointer()
.on_click(move |ctx| {
handler(&url_for_click, ctx);
});
Self {
inner,
url,
css_element_id: None,
css_classes: Vec::new(),
}
}
pub fn text_color(self, color: Color) -> Self {
self.rebuild_with_config(|cfg| cfg.text_color = color)
}
pub fn hover_color(self, color: Color) -> Self {
self.rebuild_with_config(|cfg| cfg.hover_color = color)
}
pub fn font_size(self, size: f32) -> Self {
self.rebuild_with_config(|cfg| cfg.font_size = size)
}
pub fn underline(self, enabled: bool) -> Self {
self.rebuild_with_config(|cfg| cfg.underline = enabled)
}
pub fn no_underline(self) -> Self {
self.underline(false)
}
pub fn underline_on_hover(self) -> Self {
self.rebuild_with_config(|cfg| {
cfg.underline = true;
cfg.underline_on_hover_only = true;
})
}
pub fn url(&self) -> &str {
&self.url
}
pub fn id(mut self, id: &str) -> Self {
self.css_element_id = Some(id.to_string());
self
}
pub fn class(mut self, name: &str) -> Self {
self.css_classes.push(name.to_string());
self
}
fn rebuild_with_config(self, modify: impl FnOnce(&mut LinkConfig)) -> Self {
let label = self.extract_label();
let url = self.url;
let css_element_id = self.css_element_id;
let css_classes = self.css_classes;
let mut config = LinkConfig::default();
modify(&mut config);
let mut text_element = text(&label)
.size(config.font_size)
.color(config.text_color)
.no_cursor();
if config.underline && !config.underline_on_hover_only {
text_element = text_element.underline();
}
let url_for_click = url.clone();
let inner = div()
.child(text_element)
.cursor_pointer()
.on_click(move |_ctx| {
open_url(&url_for_click);
});
Self {
inner,
url,
css_element_id,
css_classes,
}
}
fn extract_label(&self) -> String {
if let Some(child) = self.inner.children_builders().first() {
if let Some(info) = child.text_render_info() {
return info.content;
}
}
String::new()
}
}
impl Deref for Link {
type Target = Div;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl DerefMut for Link {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
impl ElementBuilder for Link {
fn build(&self, tree: &mut LayoutTree) -> LayoutNodeId {
self.inner.build(tree)
}
fn render_props(&self) -> RenderProps {
self.inner.render_props()
}
fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
self.inner.children_builders()
}
fn element_type_id(&self) -> crate::div::ElementTypeId {
self.inner.element_type_id()
}
fn semantic_type_name(&self) -> Option<&'static str> {
Some("a")
}
fn element_id(&self) -> Option<&str> {
self.css_element_id.as_deref()
}
fn element_classes(&self) -> &[String] {
&self.css_classes
}
fn event_handlers(&self) -> Option<&crate::event_handler::EventHandlers> {
ElementBuilder::event_handlers(&self.inner)
}
fn layout_style(&self) -> Option<&taffy::Style> {
self.inner.layout_style()
}
}
pub fn link(label: impl Into<String>, url: impl Into<String>) -> Link {
Link::new(label, url)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::element::CursorStyle;
fn init_theme() {
let _ = ThemeState::try_get().unwrap_or_else(|| {
ThemeState::init_default();
ThemeState::get()
});
}
#[test]
fn test_link_creates_element() {
init_theme();
let mut tree = LayoutTree::new();
let lnk = link("Test", "https://example.com");
lnk.build(&mut tree);
assert!(!tree.is_empty());
}
#[test]
fn test_link_stores_url() {
init_theme();
let lnk = link("Test", "https://example.com");
assert_eq!(lnk.url(), "https://example.com");
}
#[test]
fn test_link_has_pointer_cursor() {
init_theme();
let lnk = link("Test", "https://example.com");
let props = lnk.render_props();
assert_eq!(props.cursor, Some(CursorStyle::Pointer));
}
}