use hikari_palette::classes::{ClassesBuilder, ImageClass, TypedClass};
use crate::prelude::*;
use crate::style_builder::{CssProperty, StyleStringBuilder};
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum ImageFit {
Contain,
#[default]
Cover,
Fill,
None,
ScaleDown,
}
impl ImageFit {
pub fn as_str(&self) -> &'static str {
match self {
ImageFit::Contain => "contain",
ImageFit::Cover => "cover",
ImageFit::Fill => "fill",
ImageFit::None => "none",
ImageFit::ScaleDown => "scale-down",
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum ImagePlaceholder {
#[default]
Skeleton,
Icon,
None,
}
#[component]
pub fn Image(
src: String,
#[props(default = "Image".to_string())] alt: String,
#[props(default = ImageFit::Cover)] fit: ImageFit,
#[props(default)] width: Option<u32>,
#[props(default)] height: Option<u32>,
#[props(default)] max_width: Option<u32>,
#[props(default = false)] responsive: bool,
#[props(default)] placeholder: ImagePlaceholder,
#[props(default = true)] show_loading: bool,
#[props(default)] class: String,
) -> Element {
let mut loaded = use_signal(|| false);
let mut has_error = use_signal(|| false);
let mut builder = StyleStringBuilder::new().add(CssProperty::ObjectFit, fit.as_str());
if let Some(w) = width {
builder = builder.add_px(CssProperty::Width, w);
}
if let Some(h) = height {
builder = builder.add_px(CssProperty::Height, h);
}
if let Some(mw) = max_width {
builder = builder.add_px(CssProperty::MaxWidth, mw);
}
if responsive {
builder = builder
.add(CssProperty::Width, "100%")
.add(CssProperty::Height, "auto");
}
let style = builder.build_clean();
let classes = ClassesBuilder::new()
.add_typed(ImageClass::Image)
.add(&class)
.build();
let container_style = if responsive {
"width: 100%; position: relative; display: inline-block;".to_string()
} else if let (Some(w), Some(h)) = (width, height) {
format!(
"width: {}px; height: {}px; position: relative; display: inline-block;",
w, h
)
} else {
"position: relative; display: inline-block;".to_string()
};
let show_placeholder = !loaded.read() || has_error.read();
let placeholder_type = placeholder;
let show_skeleton =
show_placeholder && show_loading && placeholder_type == ImagePlaceholder::Skeleton;
let show_icon_placeholder =
show_placeholder && show_loading && placeholder_type == ImagePlaceholder::Icon;
let handle_load = move |_| {
loaded.set(true);
};
let handle_error = move |_| {
has_error.set(true);
};
let placeholder_el = if show_skeleton {
Some(rsx! {
div {
class: ClassesBuilder::new()
.add_typed(ImageClass::ImagePlaceholder)
.add_typed(ImageClass::ImageSkeleton)
.build(),
style: "width: 100%; height: 100%; min-height: 100px;",
}
})
} else if show_icon_placeholder {
Some(rsx! {
div {
class: ClassesBuilder::new()
.add_typed(ImageClass::ImagePlaceholder)
.add_typed(ImageClass::ImageIconPlaceholder)
.build(),
style: "width: 100%; height: 100%; min-height: 100px; display: flex; align-items: center; justify-content: center; background: var(--hi-color-surface);",
svg {
width: "48",
height: "48",
view_box: "0 0 24 24",
fill: "none",
stroke: "var(--hi-color-text-secondary)",
stroke_width: "1.5",
rect {
x: "3",
y: "3",
width: "18",
height: "18",
rx: "2",
ry: "2",
}
circle { cx: "8.5", cy: "8.5", r: "1.5" }
polyline { points: "21 15 16 10 5 21" }
}
}
})
} else {
None
};
rsx! {
div {
class: ImageClass::ImageContainer.class_name(),
style: container_style,
{placeholder_el.unwrap_or_else(VNode::empty)}
img {
class: classes,
src,
alt,
style,
onload: handle_load,
onerror: handle_error,
}
}
}
}
#[component]
pub fn Logo(
src: String,
#[props(default = "Logo".to_string())] alt: String,
#[props(default = 40)] height: u32,
#[props(default = 160)] max_width: u32,
#[props(default)] class: String,
) -> Element {
let style = StyleStringBuilder::new()
.add_px(CssProperty::Height, height)
.add_px(CssProperty::MaxWidth, max_width)
.add(CssProperty::Width, "auto")
.add(CssProperty::ObjectFit, "contain")
.build_clean();
let classes = ClassesBuilder::new()
.add_typed(ImageClass::Logo)
.add(&class)
.build();
rsx! {
img {
class: classes,
src,
alt,
style,
}
}
}