use std::time::Duration;
use blinc_core::{Brush, Color, Shadow, Transform};
use taffy::prelude::*;
use crate::div::{ElementBuilder, ElementTypeId, ImageRenderInfo};
use crate::element::{RenderLayer, RenderProps};
use crate::tree::{LayoutNodeId, LayoutTree};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LoadingStrategy {
#[default]
Eager,
Lazy,
}
#[derive(Debug, Clone)]
pub enum Placeholder {
None,
Color(Color),
Brush(Brush),
Image(String),
Skeleton,
}
impl Default for Placeholder {
fn default() -> Self {
Placeholder::Color(Color::rgba(0.15, 0.15, 0.15, 0.5))
}
}
impl From<Color> for Placeholder {
fn from(color: Color) -> Self {
Placeholder::Color(color)
}
}
impl From<Brush> for Placeholder {
fn from(brush: Brush) -> Self {
Placeholder::Brush(brush)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ObjectFit {
#[default]
Cover,
Contain,
Fill,
ScaleDown,
None,
}
impl ObjectFit {
fn to_u8(self) -> u8 {
match self {
ObjectFit::Cover => 0,
ObjectFit::Contain => 1,
ObjectFit::Fill => 2,
ObjectFit::ScaleDown => 3,
ObjectFit::None => 4,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct ObjectPosition {
pub x: f32,
pub y: f32,
}
impl ObjectPosition {
pub const TOP_LEFT: Self = Self { x: 0.0, y: 0.0 };
pub const TOP_CENTER: Self = Self { x: 0.5, y: 0.0 };
pub const TOP_RIGHT: Self = Self { x: 1.0, y: 0.0 };
pub const CENTER_LEFT: Self = Self { x: 0.0, y: 0.5 };
pub const CENTER: Self = Self { x: 0.5, y: 0.5 };
pub const CENTER_RIGHT: Self = Self { x: 1.0, y: 0.5 };
pub const BOTTOM_LEFT: Self = Self { x: 0.0, y: 1.0 };
pub const BOTTOM_CENTER: Self = Self { x: 0.5, y: 1.0 };
pub const BOTTOM_RIGHT: Self = Self { x: 1.0, y: 1.0 };
pub fn new(x: f32, y: f32) -> Self {
Self { x, y }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct ImageFilter {
pub grayscale: f32,
pub sepia: f32,
pub brightness: f32,
pub contrast: f32,
pub saturate: f32,
pub hue_rotate: f32,
pub invert: f32,
pub blur: f32,
}
impl ImageFilter {
pub fn none() -> Self {
Self {
brightness: 1.0,
contrast: 1.0,
saturate: 1.0,
..Default::default()
}
}
fn to_array(self) -> [f32; 8] {
[
self.grayscale,
self.sepia,
self.brightness,
self.contrast,
self.saturate,
self.hue_rotate,
self.invert,
self.blur,
]
}
}
pub struct Image {
source: String,
width: f32,
height: f32,
object_fit: ObjectFit,
object_position: ObjectPosition,
opacity: f32,
border_radius: f32,
tint: [f32; 4],
filter: ImageFilter,
style: Style,
render_layer: RenderLayer,
shadow: Option<Shadow>,
transform: Option<Transform>,
loading: LoadingStrategy,
placeholder: Placeholder,
fade_duration: Duration,
border_width: f32,
border_color: Option<Color>,
}
impl Image {
pub fn new(source: impl Into<String>) -> Self {
Self {
source: source.into(),
width: 100.0,
height: 100.0,
object_fit: ObjectFit::default(),
object_position: ObjectPosition::CENTER,
opacity: 1.0,
border_radius: 0.0,
tint: [1.0, 1.0, 1.0, 1.0],
filter: ImageFilter::none(),
style: Style {
size: taffy::Size {
width: Dimension::Length(100.0),
height: Dimension::Length(100.0),
},
..Default::default()
},
render_layer: RenderLayer::default(),
shadow: None,
transform: None,
loading: LoadingStrategy::default(),
placeholder: Placeholder::default(),
fade_duration: Duration::from_millis(200),
border_width: 0.0,
border_color: None,
}
}
pub fn size(mut self, width: f32, height: f32) -> Self {
self.width = width;
self.height = height;
self.style.size.width = Dimension::Length(width);
self.style.size.height = Dimension::Length(height);
self
}
pub fn square(mut self, size: f32) -> Self {
self.width = size;
self.height = size;
self.style.size.width = Dimension::Length(size);
self.style.size.height = Dimension::Length(size);
self
}
pub fn w(mut self, width: f32) -> Self {
self.width = width;
self.style.size.width = Dimension::Length(width);
self
}
pub fn h(mut self, height: f32) -> Self {
self.height = height;
self.style.size.height = Dimension::Length(height);
self
}
pub fn w_full(mut self) -> Self {
self.style.size.width = Dimension::Percent(1.0);
self
}
pub fn h_full(mut self) -> Self {
self.style.size.height = Dimension::Percent(1.0);
self
}
pub fn fit(mut self, fit: ObjectFit) -> Self {
self.object_fit = fit;
self
}
pub fn cover(self) -> Self {
self.fit(ObjectFit::Cover)
}
pub fn contain(self) -> Self {
self.fit(ObjectFit::Contain)
}
pub fn fill(self) -> Self {
self.fit(ObjectFit::Fill)
}
pub fn scale_down(self) -> Self {
self.fit(ObjectFit::ScaleDown)
}
pub fn no_scale(self) -> Self {
self.fit(ObjectFit::None)
}
pub fn position(mut self, position: ObjectPosition) -> Self {
self.object_position = position;
self
}
pub fn position_xy(mut self, x: f32, y: f32) -> Self {
self.object_position = ObjectPosition::new(x, y);
self
}
pub fn top_left(self) -> Self {
self.position(ObjectPosition::TOP_LEFT)
}
pub fn top_center(self) -> Self {
self.position(ObjectPosition::TOP_CENTER)
}
pub fn top_right(self) -> Self {
self.position(ObjectPosition::TOP_RIGHT)
}
pub fn center_left(self) -> Self {
self.position(ObjectPosition::CENTER_LEFT)
}
pub fn center(self) -> Self {
self.position(ObjectPosition::CENTER)
}
pub fn center_right(self) -> Self {
self.position(ObjectPosition::CENTER_RIGHT)
}
pub fn bottom_left(self) -> Self {
self.position(ObjectPosition::BOTTOM_LEFT)
}
pub fn bottom_center(self) -> Self {
self.position(ObjectPosition::BOTTOM_CENTER)
}
pub fn bottom_right(self) -> Self {
self.position(ObjectPosition::BOTTOM_RIGHT)
}
pub fn opacity(mut self, opacity: f32) -> Self {
self.opacity = opacity.clamp(0.0, 1.0);
self
}
pub fn rounded(mut self, radius: f32) -> Self {
self.border_radius = radius;
self
}
pub fn circular(mut self) -> Self {
self.border_radius = self.width.min(self.height) / 2.0;
self
}
pub fn border(mut self, width: f32, color: Color) -> Self {
self.border_width = width;
self.border_color = Some(color);
self
}
pub fn tint(mut self, r: f32, g: f32, b: f32) -> Self {
self.tint = [r, g, b, self.tint[3]];
self
}
pub fn tint_rgba(mut self, r: f32, g: f32, b: f32, a: f32) -> Self {
self.tint = [r, g, b, a];
self
}
pub fn filter(mut self, filter: ImageFilter) -> Self {
self.filter = filter;
self
}
pub fn grayscale(mut self, amount: f32) -> Self {
self.filter.grayscale = amount.clamp(0.0, 1.0);
self
}
pub fn sepia(mut self, amount: f32) -> Self {
self.filter.sepia = amount.clamp(0.0, 1.0);
self
}
pub fn brightness(mut self, amount: f32) -> Self {
self.filter.brightness = amount.max(0.0);
self
}
pub fn contrast(mut self, amount: f32) -> Self {
self.filter.contrast = amount.max(0.0);
self
}
pub fn saturate(mut self, amount: f32) -> Self {
self.filter.saturate = amount.max(0.0);
self
}
pub fn hue_rotate(mut self, degrees: f32) -> Self {
self.filter.hue_rotate = degrees % 360.0;
self
}
pub fn invert(mut self, amount: f32) -> Self {
self.filter.invert = amount.clamp(0.0, 1.0);
self
}
pub fn blur(mut self, radius: f32) -> Self {
self.filter.blur = radius.max(0.0);
self
}
pub fn layer(mut self, layer: RenderLayer) -> Self {
self.render_layer = layer;
self
}
pub fn foreground(self) -> Self {
self.layer(RenderLayer::Foreground)
}
pub fn m(mut self, units: f32) -> Self {
let px = LengthPercentageAuto::Length(units * 4.0);
self.style.margin = Rect {
left: px,
right: px,
top: px,
bottom: px,
};
self
}
pub fn mx(mut self, units: f32) -> Self {
let px = LengthPercentageAuto::Length(units * 4.0);
self.style.margin.left = px;
self.style.margin.right = px;
self
}
pub fn my(mut self, units: f32) -> Self {
let px = LengthPercentageAuto::Length(units * 4.0);
self.style.margin.top = px;
self.style.margin.bottom = px;
self
}
pub fn flex_grow(mut self) -> Self {
self.style.flex_grow = 1.0;
self
}
pub fn flex_shrink(mut self) -> Self {
self.style.flex_shrink = 1.0;
self
}
pub fn self_center(mut self) -> Self {
self.style.align_self = Some(AlignSelf::Center);
self
}
pub fn shadow(mut self, shadow: Shadow) -> Self {
self.shadow = Some(shadow);
self
}
pub fn shadow_params(
self,
offset_x: f32,
offset_y: f32,
blur: f32,
color: blinc_core::Color,
) -> Self {
self.shadow(Shadow::new(offset_x, offset_y, blur, color))
}
pub fn transform(mut self, transform: Transform) -> Self {
self.transform = Some(transform);
self
}
pub fn translate(self, x: f32, y: f32) -> Self {
self.transform(Transform::translate(x, y))
}
pub fn scale(self, factor: f32) -> Self {
self.transform(Transform::scale(factor, factor))
}
pub fn rotate(self, angle: f32) -> Self {
self.transform(Transform::rotate(angle))
}
pub fn lazy(mut self) -> Self {
self.loading = LoadingStrategy::Lazy;
self
}
pub fn loading_strategy(mut self, strategy: LoadingStrategy) -> Self {
self.loading = strategy;
self
}
pub fn placeholder(mut self, placeholder: Placeholder) -> Self {
self.placeholder = placeholder;
self
}
pub fn placeholder_color(mut self, color: Color) -> Self {
self.placeholder = Placeholder::Color(color);
self
}
pub fn placeholder_brush(mut self, brush: impl Into<Brush>) -> Self {
self.placeholder = Placeholder::Brush(brush.into());
self
}
pub fn placeholder_image(mut self, source: impl Into<String>) -> Self {
self.placeholder = Placeholder::Image(source.into());
self
}
pub fn skeleton(mut self) -> Self {
self.placeholder = Placeholder::Skeleton;
self
}
pub fn fade_in(mut self, duration: Duration) -> Self {
self.fade_duration = duration;
self
}
pub fn no_fade(mut self) -> Self {
self.fade_duration = Duration::ZERO;
self
}
pub fn get_loading_strategy(&self) -> LoadingStrategy {
self.loading
}
pub fn get_placeholder(&self) -> &Placeholder {
&self.placeholder
}
pub fn get_fade_duration(&self) -> Duration {
self.fade_duration
}
pub fn is_lazy(&self) -> bool {
matches!(self.loading, LoadingStrategy::Lazy)
}
pub fn source(&self) -> &str {
&self.source
}
pub fn width(&self) -> f32 {
self.width
}
pub fn height(&self) -> f32 {
self.height
}
}
impl ElementBuilder for Image {
fn build(&self, tree: &mut LayoutTree) -> LayoutNodeId {
tree.create_node(self.style.clone())
}
#[allow(deprecated)]
fn render_props(&self) -> RenderProps {
RenderProps {
border_radius: blinc_core::CornerRadius::uniform(self.border_radius),
border_color: self.border_color,
border_width: self.border_width,
layer: self.render_layer,
shadow: self.shadow,
transform: self.transform.clone(),
opacity: self.opacity,
..Default::default()
}
}
fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
&[] }
fn element_type_id(&self) -> ElementTypeId {
ElementTypeId::Image
}
fn semantic_type_name(&self) -> Option<&'static str> {
Some("img")
}
fn image_render_info(&self) -> Option<ImageRenderInfo> {
let (placeholder_type, placeholder_color, placeholder_image) = match &self.placeholder {
Placeholder::None => (0, [0.0, 0.0, 0.0, 0.0], None),
Placeholder::Color(c) => (1, [c.r, c.g, c.b, c.a], None),
Placeholder::Brush(brush) => {
let color = match brush {
Brush::Solid(c) => [c.r, c.g, c.b, c.a],
Brush::Gradient(g) => {
if let Some(stop) = g.stops().first() {
let c = &stop.color;
[c.r, c.g, c.b, c.a]
} else {
[0.2, 0.2, 0.2, 1.0]
}
}
Brush::Glass(_) => [0.1, 0.1, 0.1, 0.5], Brush::Image(_) => [0.0, 0.0, 0.0, 0.0],
Brush::Blur(blur) => {
if let Some(tint) = &blur.tint {
[tint.r, tint.g, tint.b, tint.a * blur.opacity]
} else {
[0.0, 0.0, 0.0, 0.0]
}
}
};
(4, color, None) }
Placeholder::Image(src) => (2, [0.0, 0.0, 0.0, 0.0], Some(src.clone())),
Placeholder::Skeleton => (3, [0.0, 0.0, 0.0, 0.0], None),
};
Some(ImageRenderInfo {
source: self.source.clone(),
object_fit: self.object_fit.to_u8(),
object_position: [self.object_position.x, self.object_position.y],
opacity: self.opacity,
border_radius: self.border_radius,
tint: self.tint,
filter: self.filter.to_array(),
loading_strategy: match self.loading {
LoadingStrategy::Eager => 0,
LoadingStrategy::Lazy => 1,
},
placeholder_type,
placeholder_color,
placeholder_image,
fade_duration_ms: self.fade_duration.as_millis() as u32,
})
}
fn layout_style(&self) -> Option<&taffy::Style> {
Some(&self.style)
}
}
pub fn img(source: impl Into<String>) -> Image {
Image::new(source)
}
pub fn image(source: impl Into<String>) -> Image {
Image::new(source)
}
pub fn emoji(emoji_char: impl Into<String>) -> Image {
let emoji_str = emoji_char.into();
Image::new(format!("emoji://{}", emoji_str))
.size(64.0, 64.0)
.lazy() .no_fade() }
pub fn emoji_sized(emoji_char: impl Into<String>, size: f32) -> Image {
let emoji_str = emoji_char.into();
Image::new(format!("emoji://{}?size={}", emoji_str, size))
.size(size, size)
.lazy() .no_fade() }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_image_builder() {
let i = img("test.png")
.size(200.0, 150.0)
.cover()
.rounded(8.0)
.opacity(0.9);
assert_eq!(i.source(), "test.png");
assert_eq!(i.width(), 200.0);
assert_eq!(i.height(), 150.0);
}
#[test]
fn test_image_filters() {
let i = img("test.png").grayscale(0.5).brightness(1.2).contrast(1.1);
let info = i.image_render_info().unwrap();
assert_eq!(info.filter[0], 0.5); assert_eq!(info.filter[2], 1.2); assert_eq!(info.filter[3], 1.1); }
#[test]
fn test_image_build() {
let i = img("test.png");
let mut tree = LayoutTree::new();
let _node = i.build(&mut tree);
assert_eq!(tree.len(), 1);
}
#[test]
fn test_lazy_loading() {
let i = img("large-photo.jpg")
.lazy()
.placeholder_color(Color::GRAY)
.fade_in(Duration::from_millis(300));
assert!(i.is_lazy());
assert_eq!(i.get_fade_duration(), Duration::from_millis(300));
let info = i.image_render_info().unwrap();
assert_eq!(info.loading_strategy, 1); assert_eq!(info.placeholder_type, 1); assert_eq!(info.fade_duration_ms, 300);
}
#[test]
fn test_eager_loading_default() {
let i = img("photo.jpg");
assert!(!i.is_lazy());
let info = i.image_render_info().unwrap();
assert_eq!(info.loading_strategy, 0); }
#[test]
fn test_placeholder_image() {
let i = img("large.jpg").lazy().placeholder_image("thumbnail.jpg");
let info = i.image_render_info().unwrap();
assert_eq!(info.placeholder_type, 2); assert_eq!(info.placeholder_image, Some("thumbnail.jpg".to_string()));
}
#[test]
fn test_skeleton_placeholder() {
let i = img("large.jpg").lazy().skeleton();
let info = i.image_render_info().unwrap();
assert_eq!(info.placeholder_type, 3); }
#[test]
fn test_no_fade() {
let i = img("photo.jpg").lazy().no_fade();
assert_eq!(i.get_fade_duration(), Duration::ZERO);
assert_eq!(i.image_render_info().unwrap().fade_duration_ms, 0);
}
}