use std::{borrow::Cow, iter::successors};
use image::{GenericImageView, Rgba, RgbaImage};
use smallvec::{SmallVec, smallvec};
use taffy::Size;
use crate::{
Result,
layout::{node::resolve_image, style::*},
rendering::{BorderProperties, BufferPool, MaskMemory, RenderContext, Sizing, overlay_image},
};
pub(crate) struct TileLayer {
pub blend_mode: BlendMode,
pub tile: BackgroundTile,
pub xs: SmallVec<[i32; 1]>,
pub ys: SmallVec<[i32; 1]>,
}
pub(crate) type TileLayers = Vec<TileLayer>;
pub(crate) fn rasterize_layers(
layers: TileLayers,
size: Size<u32>,
context: &RenderContext,
border: BorderProperties,
transform: Affine,
mask_memory: &mut MaskMemory,
buffer_pool: &mut BufferPool,
) -> Result<Option<BackgroundTile>> {
if layers.is_empty() {
return Ok(None);
}
let mut composed = buffer_pool.acquire_image(size.width, size.height)?;
for layer in layers {
for &x in &layer.xs {
for &y in &layer.ys {
overlay_image(
&mut composed,
&layer.tile,
border,
Affine::translation(x as f32, y as f32) * transform,
context.style.image_rendering,
layer.blend_mode,
&[],
mask_memory,
buffer_pool,
);
}
}
}
Ok(Some(BackgroundTile::Image(composed)))
}
pub(crate) struct ColorTile {
pub color: Rgba<u8>,
pub width: u32,
pub height: u32,
}
impl GenericImageView for ColorTile {
type Pixel = Rgba<u8>;
fn dimensions(&self) -> (u32, u32) {
(self.width, self.height)
}
fn get_pixel(&self, _x: u32, _y: u32) -> Self::Pixel {
self.color
}
}
pub(crate) enum BackgroundTile {
Linear(LinearGradientTile),
Radial(RadialGradientTile),
Conic(ConicGradientTile),
Noise(NoiseV1Tile),
Image(RgbaImage),
Color(ColorTile),
}
impl GenericImageView for BackgroundTile {
type Pixel = Rgba<u8>;
fn dimensions(&self) -> (u32, u32) {
match self {
Self::Linear(t) => t.dimensions(),
Self::Radial(t) => t.dimensions(),
Self::Conic(t) => t.dimensions(),
Self::Noise(t) => t.dimensions(),
Self::Image(t) => t.dimensions(),
Self::Color(t) => t.dimensions(),
}
}
fn get_pixel(&self, x: u32, y: u32) -> Self::Pixel {
match self {
Self::Linear(t) => t.get_pixel(x, y),
Self::Radial(t) => t.get_pixel(x, y),
Self::Conic(t) => t.get_pixel(x, y),
Self::Noise(t) => t.get_pixel(x, y),
Self::Image(t) => *t.get_pixel(x, y),
Self::Color(t) => t.color,
}
}
}
impl BackgroundTile {
pub(crate) fn as_raw(&self) -> Option<&[u8]> {
match self {
Self::Image(image) => Some(image.as_raw()),
_ => None,
}
}
}
pub(crate) fn resolve_length_against_area(unit: Length, area: u32, sizing: &Sizing) -> u32 {
match unit {
Length::Auto => area,
_ => unit.to_px(sizing, area as f32).max(0.0) as u32,
}
}
pub(crate) fn resolve_background_size(
size: BackgroundSize,
area: Size<u32>,
image: &BackgroundImage,
context: &RenderContext,
) -> (u32, u32) {
match size {
BackgroundSize::Explicit { width, height } => (
resolve_length_against_area(width, area.width, &context.sizing),
resolve_length_against_area(height, area.height, &context.sizing),
),
BackgroundSize::Cover => {
let (intrinsic_width, intrinsic_height) = if let BackgroundImage::Url(url) = image
&& let Ok(source) = resolve_image(url, context)
{
source.size()
} else {
return (0, 0);
};
if intrinsic_width == 0.0 || intrinsic_height == 0.0 {
return (0, 0);
}
let scale_x = area.width as f32 / intrinsic_width;
let scale_y = area.height as f32 / intrinsic_height;
let scale = scale_x.max(scale_y);
(
(intrinsic_width * scale).round() as u32,
(intrinsic_height * scale).round() as u32,
)
}
BackgroundSize::Contain => {
let (intrinsic_width, intrinsic_height) = if let BackgroundImage::Url(url) = image
&& let Ok(source) = resolve_image(url, context)
{
source.size()
} else {
return (0, 0);
};
if intrinsic_width == 0.0 || intrinsic_height == 0.0 {
return (0, 0);
}
let scale_x = area.width as f32 / intrinsic_width;
let scale_y = area.height as f32 / intrinsic_height;
let scale = scale_x.min(scale_y);
(
(intrinsic_width * scale).round() as u32,
(intrinsic_height * scale).round() as u32,
)
}
}
}
pub(crate) fn resolve_length_to_position_component(
length: Length,
available: i32,
sizing: &Sizing,
) -> i32 {
match length {
Length::Auto => available / 2,
_ => length.to_px(sizing, available as f32) as i32,
}
}
pub(crate) fn resolve_position_component_x(
comp: BackgroundPosition,
tile_w: u32,
area_w: u32,
sizing: &Sizing,
) -> i32 {
let available = area_w.saturating_sub(tile_w) as i32;
match comp.0.x {
PositionComponent::KeywordX(PositionKeywordX::Left) => 0,
PositionComponent::KeywordX(PositionKeywordX::Center) => available / 2,
PositionComponent::KeywordX(PositionKeywordX::Right) => available,
PositionComponent::KeywordY(_) => available / 2,
PositionComponent::Length(length) => {
resolve_length_to_position_component(length, available, sizing)
}
}
}
pub(crate) fn resolve_position_component_y(
comp: BackgroundPosition,
tile_h: u32,
area_h: u32,
sizing: &Sizing,
) -> i32 {
let available = area_h.saturating_sub(tile_h) as i32;
match comp.0.y {
PositionComponent::KeywordY(PositionKeywordY::Top) => 0,
PositionComponent::KeywordY(PositionKeywordY::Center) => available / 2,
PositionComponent::KeywordY(PositionKeywordY::Bottom) => available,
PositionComponent::KeywordX(_) => available / 2,
PositionComponent::Length(length) => {
resolve_length_to_position_component(length, available, sizing)
}
}
}
pub(crate) fn render_tile(
image: &BackgroundImage,
tile_w: u32,
tile_h: u32,
context: &RenderContext,
buffer_pool: &mut BufferPool,
) -> Result<Option<BackgroundTile>> {
Ok(match image {
BackgroundImage::None => None,
BackgroundImage::Linear(gradient) => Some(BackgroundTile::Linear(LinearGradientTile::new(
gradient,
tile_w,
tile_h,
context,
buffer_pool,
))),
BackgroundImage::Radial(gradient) => Some(BackgroundTile::Radial(RadialGradientTile::new(
gradient,
tile_w,
tile_h,
context,
buffer_pool,
))),
BackgroundImage::Conic(gradient) => Some(BackgroundTile::Conic(ConicGradientTile::new(
gradient,
tile_w,
tile_h,
context,
buffer_pool,
))),
BackgroundImage::Noise(noise) => Some(BackgroundTile::Noise(NoiseV1Tile::new(
*noise, tile_w, tile_h,
))),
BackgroundImage::Url(url) => {
if let Ok(source) = resolve_image(url, context) {
Some(BackgroundTile::Image(
source
.render_to_rgba_image(
tile_w,
tile_h,
context.style.image_rendering,
context.current_color,
)?
.into_owned(),
))
} else {
None
}
}
})
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn resolve_layer_tiles(
image: &BackgroundImage,
pos: BackgroundPosition,
size: BackgroundSize,
repeat: BackgroundRepeat,
blend_mode: BlendMode,
area: Size<u32>,
context: &RenderContext,
buffer_pool: &mut BufferPool,
) -> Result<Option<TileLayer>> {
let (initial_w, initial_h) = resolve_background_size(size, area, image, context);
if initial_w == 0 || initial_h == 0 {
return Ok(None);
}
let (xs, tile_w) = match repeat.0 {
BackgroundRepeatStyle::Repeat => {
let origin_x = resolve_position_component_x(pos, initial_w, area.width, &context.sizing);
(
collect_repeat_tile_positions(area.width, initial_w, origin_x),
initial_w,
)
}
BackgroundRepeatStyle::NoRepeat => {
let origin_x = resolve_position_component_x(pos, initial_w, area.width, &context.sizing);
(smallvec![origin_x], initial_w)
}
BackgroundRepeatStyle::Space => (
collect_spaced_tile_positions(area.width, initial_w),
initial_w,
),
BackgroundRepeatStyle::Round => collect_stretched_tile_positions(area.width, initial_w),
};
let (ys, tile_h) = match repeat.1 {
BackgroundRepeatStyle::Repeat => {
let origin_y = resolve_position_component_y(pos, initial_h, area.height, &context.sizing);
(
collect_repeat_tile_positions(area.height, initial_h, origin_y),
initial_h,
)
}
BackgroundRepeatStyle::NoRepeat => {
let origin_y = resolve_position_component_y(pos, initial_h, area.height, &context.sizing);
(smallvec![origin_y], initial_h)
}
BackgroundRepeatStyle::Space => (
collect_spaced_tile_positions(area.height, initial_h),
initial_h,
),
BackgroundRepeatStyle::Round => collect_stretched_tile_positions(area.height, initial_h),
};
if xs.is_empty() || ys.is_empty() {
return Ok(None);
}
let Some(tile) = render_tile(image, tile_w, tile_h, context, buffer_pool)? else {
return Ok(None);
};
Ok(Some(TileLayer {
tile,
xs,
ys,
blend_mode,
}))
}
pub(crate) fn collect_repeat_tile_positions(
area_size: u32,
tile_size: u32,
origin: i32,
) -> SmallVec<[i32; 1]> {
if tile_size == 0 {
return SmallVec::default();
}
let mut start = origin;
if start > 0 {
let n = ((start as f32) / tile_size as f32).ceil() as i32;
start -= n * tile_size as i32;
}
successors(Some(start), |&x| Some(x + tile_size as i32))
.take_while(|&x| x < area_size as i32)
.collect()
}
pub(crate) fn collect_spaced_tile_positions(area_size: u32, tile_size: u32) -> SmallVec<[i32; 1]> {
if tile_size == 0 {
return SmallVec::default();
}
let count = area_size / tile_size;
if count <= 1 {
return smallvec![(area_size as i32 - tile_size as i32) / 2];
}
let gap = (area_size - count * tile_size) / (count - 1);
let step = tile_size as i32 + gap as i32;
successors(Some(0i32), move |&x| Some(x + step))
.take(count as usize)
.collect()
}
pub(crate) fn collect_stretched_tile_positions(
area_size: u32,
tile_size: u32,
) -> (SmallVec<[i32; 1]>, u32) {
if tile_size == 0 || area_size == 0 {
return (SmallVec::default(), tile_size);
}
let count = (area_size as f32 / tile_size as f32).max(1.0) as u32;
let new_tile_size = (area_size as f32 / count as f32) as u32;
let positions = successors(Some(0i32), move |&x| Some(x + new_tile_size as i32))
.take(count as usize)
.collect();
(positions, new_tile_size)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn resolve_tile_layers(
images: &[BackgroundImage],
positions: &[BackgroundPosition],
sizes: &[BackgroundSize],
repeats: &[BackgroundRepeat],
blend_modes: &[BlendMode],
context: &RenderContext,
border_box: Size<u32>,
buffer_pool: &mut BufferPool,
) -> Result<TileLayers> {
let last_position = positions.last().copied().unwrap_or_default();
let last_size = sizes.last().copied().unwrap_or_default();
let last_repeat = repeats.last().copied().unwrap_or_default();
let last_blend_mode = blend_modes.last().copied().unwrap_or_default();
let mut results = Vec::new();
for (i, image) in images.iter().enumerate() {
let pos = positions.get(i).copied().unwrap_or(last_position);
let size = sizes.get(i).copied().unwrap_or(last_size);
let repeat = repeats.get(i).copied().unwrap_or(last_repeat);
let blend_mode = blend_modes.get(i).copied().unwrap_or(last_blend_mode);
results.push(resolve_layer_tiles(
image,
pos,
size,
repeat,
blend_mode,
border_box,
context,
buffer_pool,
)?);
}
Ok(results.into_iter().flatten().collect())
}
pub(crate) fn create_mask(
context: &RenderContext,
border_box: Size<f32>,
mask_memory: &mut MaskMemory,
buffer_pool: &mut BufferPool,
) -> Result<Option<Vec<u8>>> {
let mask_image = context
.style
.mask_image
.as_deref()
.map(Cow::Borrowed)
.unwrap_or_else(|| {
Cow::Owned(
context
.style
.mask
.iter()
.map(|background| background.image.clone())
.collect::<Vec<_>>(),
)
});
let layers = resolve_tile_layers(
&mask_image,
&context
.style
.mask_position
.as_deref()
.map(Cow::Borrowed)
.unwrap_or_else(|| {
Cow::Owned(
context
.style
.mask
.iter()
.map(|background| background.position)
.collect::<Vec<_>>(),
)
}),
&context
.style
.mask_size
.as_deref()
.map(Cow::Borrowed)
.unwrap_or_else(|| {
Cow::Owned(
context
.style
.mask
.iter()
.map(|background| background.size)
.collect::<Vec<_>>(),
)
}),
&context
.style
.mask_repeat
.as_deref()
.map(Cow::Borrowed)
.unwrap_or_else(|| {
Cow::Owned(
context
.style
.mask
.iter()
.map(|background| background.repeat)
.collect::<Vec<_>>(),
)
}),
&[], context,
border_box.map(|x| x as u32),
buffer_pool,
)?;
if layers.is_empty() {
return Ok(None);
}
Ok(
rasterize_layers(
layers,
border_box.map(|x| x as u32),
context,
BorderProperties::default(),
Affine::IDENTITY,
mask_memory,
buffer_pool,
)?
.map(|tile| {
let (w, h) = tile.dimensions();
let mut alpha = buffer_pool.acquire_dirty((w * h) as usize);
if let Some(raw) = tile.as_raw() {
let count = alpha.len().min(raw.len() / 4);
for i in 0..count {
alpha[i] = raw[i * 4 + 3];
}
for alpha_val in alpha.iter_mut().skip(count) {
*alpha_val = 0;
}
} else {
let mut i = 0;
for y in 0..h {
for x in 0..w {
if i < alpha.len() {
alpha[i] = tile.get_pixel(x, y).0[3];
i += 1;
}
}
}
for alpha_val in alpha.iter_mut().skip(i) {
*alpha_val = 0;
}
}
if let BackgroundTile::Image(image) = tile {
buffer_pool.release_image(image);
}
alpha
}),
)
}
pub(crate) fn collect_background_layers(
context: &RenderContext,
border_box: Size<f32>,
buffer_pool: &mut BufferPool,
) -> Result<TileLayers> {
let background_image = context
.style
.background_image
.as_deref()
.map(Cow::Borrowed)
.unwrap_or_else(|| {
Cow::Owned(
context
.style
.background
.iter()
.map(|background| background.image.clone())
.collect::<Vec<_>>(),
)
});
let mut layers = resolve_tile_layers(
&background_image,
&context
.style
.background_position
.as_deref()
.map(Cow::Borrowed)
.unwrap_or_else(|| {
Cow::Owned(
context
.style
.background
.iter()
.map(|background| background.position)
.collect::<Vec<_>>(),
)
}),
&context
.style
.background_size
.as_deref()
.map(Cow::Borrowed)
.unwrap_or_else(|| {
Cow::Owned(
context
.style
.background
.iter()
.map(|background| background.size)
.collect::<Vec<_>>(),
)
}),
&context
.style
.background_repeat
.as_deref()
.map(Cow::Borrowed)
.unwrap_or_else(|| {
Cow::Owned(
context
.style
.background
.iter()
.map(|background| background.repeat)
.collect::<Vec<_>>(),
)
}),
&context
.style
.background_blend_mode
.as_deref()
.map(Cow::Borrowed)
.unwrap_or_else(|| {
Cow::Owned(
context
.style
.background
.iter()
.map(|background| background.blend_mode)
.collect::<Vec<_>>(),
)
}),
context,
border_box.map(|x| x as u32),
buffer_pool,
)?;
let background_color = context
.style
.background_color()
.resolve(context.current_color);
if background_color.0[3] > 0 {
layers.insert(
0,
TileLayer {
tile: BackgroundTile::Color(ColorTile {
color: background_color.into(),
width: border_box.width as u32,
height: border_box.height as u32,
}),
xs: [0].into(),
ys: [0].into(),
blend_mode: BlendMode::Normal,
},
);
}
Ok(layers)
}