#[allow(clippy::wildcard_imports)] use super::*;
use std::collections::HashMap;
use azul_core::geom::{LogicalPosition, LogicalRect, LogicalSize};
use azul_core::resources::{DecodedImage, ImageRef, RendererResources};
use azul_core::ui_solver::GlyphInstance;
use azul_css::props::basic::{ColorOrSystem, ColorU, FontRef};
use azul_css::props::basic::pixel::DEFAULT_FONT_SIZE;
use azul_css::props::style::filter::StyleFilter;
use azul_css::props::style::box_shadow::StyleBoxShadow;
use agg_rust::basics::{FillingRule, PATH_FLAGS_NONE};
use agg_rust::blur::stack_blur_rgba32;
use agg_rust::color::Rgba8;
use agg_rust::conv_stroke::ConvStroke;
use agg_rust::gradient_lut::GradientLut;
use agg_rust::path_storage::PathStorage;
use agg_rust::pixfmt_rgba::PixfmtRgba32;
use agg_rust::rasterizer_scanline_aa::RasterizerScanlineAa;
use agg_rust::renderer_base::RendererBase;
use agg_rust::renderer_scanline::render_scanlines_aa_solid;
use agg_rust::rendering_buffer::RowAccessor;
use agg_rust::rounded_rect::RoundedRect;
use agg_rust::scanline_u::ScanlineU8;
use agg_rust::span_gradient::{GradientConic, GradientRadialD, GradientX};
use agg_rust::trans_affine::TransAffine;
use crate::font::parsed::ParsedFont;
use crate::glyph_cache::GlyphCache;
use crate::solver3::display_list::{BorderRadius, DisplayList, DisplayListItem, LocalScrollId};
use crate::text3::cache::{FontHash, FontManager};
const MAX_SHADOW_PIXBUF_SIZE: u32 = 4096;
const SYSTEM_COLOR_FALLBACK: ColorU = ColorU {
r: 0,
g: 0,
b: 0,
a: 0,
};
#[allow(clippy::trivially_copy_pass_by_ref)] fn resolve_color(
color: &ColorOrSystem,
system_colors: Option<&azul_css::system::SystemColors>,
) -> ColorU {
match (color, system_colors) {
(ColorOrSystem::Color(c), _) => *c,
(ColorOrSystem::System(_), Some(sc)) => color.resolve(sc, SYSTEM_COLOR_FALLBACK),
(ColorOrSystem::System(_), None) => SYSTEM_COLOR_FALLBACK,
}
}
fn build_gradient_lut_linear(
stops: &azul_css::props::style::background::NormalizedLinearColorStopVec,
system_colors: Option<&azul_css::system::SystemColors>,
) -> GradientLut {
let mut lut = GradientLut::new_default();
let stops_slice = stops.as_ref();
if stops_slice.len() < 2 {
lut.add_color(0.0, Rgba8::new(0, 0, 0, 0));
lut.add_color(1.0, Rgba8::new(0, 0, 0, 0));
lut.build_lut();
return lut;
}
for stop in stops_slice {
let offset = f64::from(stop.offset.normalized()); let c = resolve_color(&stop.color, system_colors);
lut.add_color(
offset,
Rgba8::new(u32::from(c.r), u32::from(c.g), u32::from(c.b), u32::from(c.a)),
);
}
lut.build_lut();
lut
}
fn build_gradient_lut_radial(
stops: &azul_css::props::style::background::NormalizedRadialColorStopVec,
system_colors: Option<&azul_css::system::SystemColors>,
) -> GradientLut {
let mut lut = GradientLut::new_default();
let stops_slice = stops.as_ref();
if stops_slice.len() < 2 {
lut.add_color(0.0, Rgba8::new(0, 0, 0, 0));
lut.add_color(1.0, Rgba8::new(0, 0, 0, 0));
lut.build_lut();
return lut;
}
for stop in stops_slice {
let offset = f64::from((stop.angle.to_degrees() / 360.0).clamp(0.0, 1.0));
let c = resolve_color(&stop.color, system_colors);
lut.add_color(
offset,
Rgba8::new(u32::from(c.r), u32::from(c.g), u32::from(c.b), u32::from(c.a)),
);
}
lut.build_lut();
lut
}
fn resolve_background_position(
pos: &azul_css::props::style::background::StyleBackgroundPosition,
width: f32,
height: f32,
) -> (f32, f32) {
use azul_css::props::style::background::{
BackgroundPositionHorizontal, BackgroundPositionVertical,
};
let x = match pos.horizontal {
BackgroundPositionHorizontal::Left => 0.0,
BackgroundPositionHorizontal::Center => 0.5,
BackgroundPositionHorizontal::Right => 1.0,
BackgroundPositionHorizontal::Exact(px) => {
let val = px.to_pixels_internal(width, 16.0, 16.0);
if width > 0.0 {
val / width
} else {
0.5
}
}
};
let y = match pos.vertical {
BackgroundPositionVertical::Top => 0.0,
BackgroundPositionVertical::Center => 0.5,
BackgroundPositionVertical::Bottom => 1.0,
BackgroundPositionVertical::Exact(px) => {
let val = px.to_pixels_internal(height, 16.0, 16.0);
if height > 0.0 {
val / height
} else {
0.5
}
}
};
(x, y)
}
#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] fn render_linear_gradient(
pixmap: &mut AzulPixmap,
bounds: &LogicalRect,
gradient: &azul_css::props::style::background::LinearGradient,
border_radius: &BorderRadius,
clip: Option<AzRect>,
dpi_factor: f32,
system_colors: Option<&azul_css::system::SystemColors>,
) {
use azul_css::props::basic::geometry::{LayoutRect, LayoutSize};
let Some(rect) = logical_rect_to_az_rect(bounds, dpi_factor) else {
return;
};
let stops = gradient.stops.as_ref();
if stops.is_empty() {
return;
}
let lut = build_gradient_lut_linear(&gradient.stops, system_colors);
let layout_rect = LayoutRect {
origin: azul_css::props::basic::geometry::LayoutPoint::new(0, 0),
size: LayoutSize {
width: (rect.width as isize),
height: (rect.height as isize),
},
};
let (from_pt, to_pt) = gradient.direction.to_points(&layout_rect);
let x1 = f64::from(rect.x) + from_pt.x as f64;
let y1 = f64::from(rect.y) + from_pt.y as f64;
let x2 = f64::from(rect.x) + to_pt.x as f64;
let y2 = f64::from(rect.y) + to_pt.y as f64;
let dx = x2 - x1;
let dy = y2 - y1;
let len = dx.hypot(dy);
if len < 0.001 {
return;
}
let mut transform = TransAffine::new_line_segment(x1, y1, x2, y2, 100.0);
transform.invert();
let mut path = if border_radius.is_zero() {
build_rect_path(&rect)
} else {
build_rounded_rect_path(&rect, border_radius, dpi_factor)
};
agg_fill_gradient_clipped(
pixmap, &mut path, &lut, GradientX, transform, 0.0, 100.0, clip,
);
}
#[allow(clippy::suboptimal_flops)] #[allow(clippy::similar_names)] #[allow(clippy::match_same_arms)] fn render_radial_gradient(
pixmap: &mut AzulPixmap,
bounds: &LogicalRect,
gradient: &azul_css::props::style::background::RadialGradient,
border_radius: &BorderRadius,
clip: Option<AzRect>,
dpi_factor: f32,
system_colors: Option<&azul_css::system::SystemColors>,
) {
use azul_css::props::style::background::{RadialGradientSize, Shape};
let Some(rect) = logical_rect_to_az_rect(bounds, dpi_factor) else {
return;
};
let stops = gradient.stops.as_ref();
if stops.is_empty() {
return;
}
let lut = build_gradient_lut_linear(&gradient.stops, system_colors);
let w = f64::from(rect.width);
let h = f64::from(rect.height);
let (cx_frac, cy_frac) =
resolve_background_position(&gradient.position, rect.width, rect.height);
let cx = f64::from(rect.x) + f64::from(cx_frac) * w;
let cy = f64::from(rect.y) + f64::from(cy_frac) * h;
let radius = match gradient.size {
RadialGradientSize::ClosestSide => {
let dx = (f64::from(cx_frac) * w).min((1.0 - f64::from(cx_frac)) * w);
let dy = (f64::from(cy_frac) * h).min((1.0 - f64::from(cy_frac)) * h);
match gradient.shape {
Shape::Circle => dx.min(dy),
Shape::Ellipse => dx.min(dy), }
}
RadialGradientSize::FarthestSide => {
let dx = (f64::from(cx_frac) * w).max((1.0 - f64::from(cx_frac)) * w);
let dy = (f64::from(cy_frac) * h).max((1.0 - f64::from(cy_frac)) * h);
match gradient.shape {
Shape::Circle => dx.max(dy),
Shape::Ellipse => dx.max(dy),
}
}
RadialGradientSize::ClosestCorner => {
let dx = (f64::from(cx_frac) * w).min((1.0 - f64::from(cx_frac)) * w);
let dy = (f64::from(cy_frac) * h).min((1.0 - f64::from(cy_frac)) * h);
dx.hypot(dy)
}
RadialGradientSize::FarthestCorner => {
let dx = (f64::from(cx_frac) * w).max((1.0 - f64::from(cx_frac)) * w);
let dy = (f64::from(cy_frac) * h).max((1.0 - f64::from(cy_frac)) * h);
dx.hypot(dy)
}
};
if radius < 0.001 {
return;
}
let mut transform = TransAffine::new_scaling_uniform(radius / 100.0);
transform.translate(cx, cy);
transform.invert();
let mut path = if border_radius.is_zero() {
build_rect_path(&rect)
} else {
build_rounded_rect_path(&rect, border_radius, dpi_factor)
};
agg_fill_gradient_clipped(
pixmap,
&mut path,
&lut,
GradientRadialD,
transform,
0.0,
100.0,
clip,
);
}
#[allow(clippy::suboptimal_flops)] #[allow(clippy::similar_names)] fn render_conic_gradient(
pixmap: &mut AzulPixmap,
bounds: &LogicalRect,
gradient: &azul_css::props::style::background::ConicGradient,
border_radius: &BorderRadius,
clip: Option<AzRect>,
dpi_factor: f32,
system_colors: Option<&azul_css::system::SystemColors>,
) {
let Some(rect) = logical_rect_to_az_rect(bounds, dpi_factor) else {
return;
};
let stops = gradient.stops.as_ref();
if stops.is_empty() {
return;
}
let lut = build_gradient_lut_radial(&gradient.stops, system_colors);
let w = f64::from(rect.width);
let h = f64::from(rect.height);
let (cx_frac, cy_frac) = resolve_background_position(&gradient.center, rect.width, rect.height);
let cx = f64::from(rect.x) + f64::from(cx_frac) * w;
let cy = f64::from(rect.y) + f64::from(cy_frac) * h;
let start_angle_deg = gradient.angle.to_degrees();
let start_angle_rad = f64::from(start_angle_deg - 90.0).to_radians();
let mut transform = TransAffine::new_rotation(start_angle_rad);
transform.translate(cx, cy);
transform.invert();
let d2 = 100.0;
let mut path = if border_radius.is_zero() {
build_rect_path(&rect)
} else {
build_rounded_rect_path(&rect, border_radius, dpi_factor)
};
agg_fill_gradient_clipped(
pixmap,
&mut path,
&lut,
GradientConic,
transform,
0.0,
d2,
clip,
);
}
#[allow(clippy::suboptimal_flops)] #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_sign_loss)] fn render_box_shadow(
pixmap: &mut AzulPixmap,
bounds: &LogicalRect,
shadow: &StyleBoxShadow,
border_radius: &BorderRadius,
dpi_factor: f32,
) -> Result<(), String> {
use azul_css::props::style::box_shadow::BoxShadowClipMode;
let Some(rect) = logical_rect_to_az_rect(bounds, dpi_factor) else {
return Ok(());
};
let offset_x =
shadow
.offset_x
.inner
.to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)
* dpi_factor;
let offset_y =
shadow
.offset_y
.inner
.to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)
* dpi_factor;
let blur_r =
(shadow
.blur_radius
.inner
.to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)
* dpi_factor)
.max(0.0);
let spread =
shadow
.spread_radius
.inner
.to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)
* dpi_factor;
let color = shadow.color;
if color.a == 0 {
return Ok(());
}
let padding = blur_r.ceil();
let shadow_x = rect.x + offset_x - spread - padding;
let shadow_y = rect.y + offset_y - spread - padding;
let shadow_w = rect.width + 2.0 * spread + 2.0 * padding;
let shadow_h = rect.height + 2.0 * spread + 2.0 * padding;
if shadow_w <= 0.0 || shadow_h <= 0.0 {
return Ok(());
}
let sw = shadow_w.ceil() as u32;
let sh = shadow_h.ceil() as u32;
if sw == 0 || sh == 0 || sw > MAX_SHADOW_PIXBUF_SIZE || sh > MAX_SHADOW_PIXBUF_SIZE {
return Ok(());
}
let mut tmp = AzulPixmap::new(sw, sh).ok_or("cannot create shadow pixmap")?;
tmp.fill(0, 0, 0, 0);
let shape_x = padding + spread;
let shape_y = padding + spread;
let Some(shape_rect) = AzRect::from_xywh(shape_x, shape_y, rect.width, rect.height) else {
return Ok(());
};
let agg_color = Rgba8::new(
u32::from(color.r),
u32::from(color.g),
u32::from(color.b),
u32::from(color.a),
);
if border_radius.is_zero() {
let mut path = build_rect_path(&shape_rect);
agg_fill_path(&mut tmp, &mut path, &agg_color, FillingRule::NonZero);
} else {
let mut path = build_rounded_rect_path(&shape_rect, border_radius, dpi_factor);
agg_fill_path(&mut tmp, &mut path, &agg_color, FillingRule::NonZero);
}
if blur_r > 0.5 {
let blur_radius = (blur_r.ceil() as u32).min(254);
let stride = (sw * 4) as i32;
let mut ra = unsafe { RowAccessor::new_with_buf(tmp.data.as_mut_ptr(), sw, sh, stride) };
stack_blur_rgba32(&mut ra, blur_radius, blur_radius);
}
let dst_x = shadow_x as i32;
let dst_y = shadow_y as i32;
blit_buffer(pixmap, &tmp.data, sw, sh, dst_x, dst_y);
Ok(())
}
#[derive(Debug)]
pub enum MaskEntry {
ImageMask {
snapshot: Vec<u8>,
mask_data: Vec<u8>,
origin_x: i32,
origin_y: i32,
width: u32,
height: u32,
},
Opacity {
snapshot: Vec<u8>,
rect: AzRect,
opacity: f32,
},
}
#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss, clippy::cast_sign_loss)] fn extract_mask_data(mask_image: &ImageRef, target_w: u32, target_h: u32) -> Option<Vec<u8>> {
let image_data = mask_image.get_data();
let (mask_bytes, src_w, src_h) = match image_data {
DecodedImage::Raw((descriptor, data)) => {
let w = descriptor.width as u32;
let h = descriptor.height as u32;
if w == 0 || h == 0 {
return None;
}
let bytes = match data {
azul_core::resources::ImageData::Raw(shared) => shared.as_ref(),
azul_core::resources::ImageData::External(_) => return None,
};
match descriptor.format {
azul_core::resources::RawImageFormat::R8 => (bytes.to_vec(), w, h),
azul_core::resources::RawImageFormat::BGRA8 => {
let mut r8 = Vec::with_capacity((w * h) as usize);
for chunk in bytes.chunks_exact(4) {
r8.push(chunk[3]); }
(r8, w, h)
}
_ => {
let chan_count = bytes.len() / (w * h) as usize;
if chan_count == 0 {
return None;
}
let mut r8 = Vec::with_capacity((w * h) as usize);
for i in 0..(w * h) as usize {
r8.push(bytes[i * chan_count]);
}
(r8, w, h)
}
}
}
_ => return None,
};
if target_w == 0 || target_h == 0 {
return None;
}
let mut scaled = vec![0u8; (target_w * target_h) as usize];
let sx = src_w as f32 / target_w as f32;
let sy = src_h as f32 / target_h as f32;
for py in 0..target_h {
for px in 0..target_w {
let mx = ((px as f32 * sx) as u32).min(src_w - 1);
let my = ((py as f32 * sy) as u32).min(src_h - 1);
scaled[(py * target_w + px) as usize] = mask_bytes[(my * src_w + mx) as usize];
}
}
Some(scaled)
}
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_sign_loss)] fn apply_mask(pixmap: &mut AzulPixmap, entry: &MaskEntry) {
let (snapshot, mask_data, origin_x, origin_y, width, height) = match entry {
MaskEntry::ImageMask {
snapshot,
mask_data,
origin_x,
origin_y,
width,
height,
} => (
snapshot,
mask_data.as_slice(),
*origin_x,
*origin_y,
*width,
*height,
),
MaskEntry::Opacity{ .. } => return,
};
let pw = pixmap.width as i32;
let ph = pixmap.height as i32;
for py in 0..height as i32 {
let dy = origin_y + py;
if dy < 0 || dy >= ph {
continue;
}
for px in 0..width as i32 {
let dx = origin_x + px;
if dx < 0 || dx >= pw {
continue;
}
let mi = (py as u32 * width + px as u32) as usize;
let mask_val = u32::from(mask_data.get(mi).copied().unwrap_or(0));
let pi = ((dy as u32 * pixmap.width + dx as u32) * 4) as usize;
let si = ((py as u32 * width + px as u32) * 4) as usize;
if pi + 3 >= pixmap.data.len() || si + 3 >= snapshot.len() {
continue;
}
let inv_mask = 255 - mask_val;
for c in 0..4 {
let snap_c = u32::from(snapshot[si + c]);
let cur_c = u32::from(pixmap.data[pi + c]);
pixmap.data[pi + c] = ((cur_c * mask_val + snap_c * inv_mask) / 255) as u8;
}
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct RenderOptions {
pub width: f32,
pub height: f32,
pub dpi_factor: f32,
}
fn acquire_pixmap(retained: Option<AzulPixmap>, w: u32, h: u32) -> Result<AzulPixmap, String> {
if let Some(p) = retained {
if p.width == w && p.height == h {
return Ok(p);
}
}
AzulPixmap::new(w, h).ok_or_else(|| "cannot create pixmap".to_string())
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] pub fn render(
dl: &DisplayList,
res: &RendererResources,
opts: RenderOptions,
glyph_cache: &mut GlyphCache,
) -> Result<AzulPixmap, String> {
let RenderOptions {
width,
height,
dpi_factor,
} = opts;
let mut pixmap = acquire_pixmap(
None,
(width * dpi_factor) as u32,
(height * dpi_factor) as u32,
)?;
pixmap.fill(255, 255, 255, 255);
render_display_list(dl, &mut pixmap, dpi_factor, res, None, glyph_cache)?;
Ok(pixmap)
}
pub fn render_with_font_manager(
dl: &DisplayList,
res: &RendererResources,
font_manager: &FontManager<FontRef>,
opts: RenderOptions,
glyph_cache: &mut GlyphCache,
) -> Result<AzulPixmap, String> {
let empty_state = CpuRenderState::new(ScrollOffsetMap::new());
render_with_font_manager_and_scroll(dl, res, font_manager, opts, glyph_cache, &empty_state)
}
pub fn render_with_font_manager_and_scroll(
dl: &DisplayList,
res: &RendererResources,
font_manager: &FontManager<FontRef>,
opts: RenderOptions,
glyph_cache: &mut GlyphCache,
render_state: &CpuRenderState,
) -> Result<AzulPixmap, String> {
render_with_font_manager_and_scroll_retained(
dl,
res,
font_manager,
opts,
glyph_cache,
render_state,
None,
)
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] pub fn render_with_font_manager_and_scroll_retained(
dl: &DisplayList,
res: &RendererResources,
font_manager: &FontManager<FontRef>,
opts: RenderOptions,
glyph_cache: &mut GlyphCache,
render_state: &CpuRenderState,
retained: Option<AzulPixmap>,
) -> Result<AzulPixmap, String> {
let RenderOptions {
width,
height,
dpi_factor,
} = opts;
let pw = (width * dpi_factor) as u32;
let ph = (height * dpi_factor) as u32;
let mut pixmap = acquire_pixmap(retained, pw, ph)?;
pixmap.fill(255, 255, 255, 255);
render_display_list_with_state(
dl,
&mut pixmap,
dpi_factor,
res,
Some(font_manager),
glyph_cache,
render_state,
)?;
Ok(pixmap)
}
pub type ScrollOffsetMap = HashMap<LocalScrollId, (f32, f32)>;
#[derive(Debug)]
pub struct CpuRenderState {
pub scroll_offsets: ScrollOffsetMap,
pub transforms: HashMap<usize, azul_core::transform::ComputedTransform3D>,
pub opacities: HashMap<usize, f32>,
pub system_style: Option<std::sync::Arc<azul_css::system::SystemStyle>>,
pub virtual_view_display_lists:
std::collections::BTreeMap<azul_core::dom::DomId, std::sync::Arc<DisplayList>>,
pub image_callback_results:
std::collections::BTreeMap<azul_core::resources::ImageRefHash, ImageRef>,
}
impl CpuRenderState {
#[must_use] pub fn new(scroll_offsets: ScrollOffsetMap) -> Self {
Self {
scroll_offsets,
transforms: HashMap::new(),
opacities: HashMap::new(),
system_style: None,
virtual_view_display_lists: std::collections::BTreeMap::new(),
image_callback_results: std::collections::BTreeMap::new(),
}
}
#[must_use] pub fn with_image_callback_results(
mut self,
results: std::collections::BTreeMap<
azul_core::resources::ImageRefHash,
ImageRef,
>,
) -> Self {
self.image_callback_results = results;
self
}
#[must_use] pub fn with_virtual_view_display_lists(
mut self,
lists: std::collections::BTreeMap<azul_core::dom::DomId, std::sync::Arc<DisplayList>>,
) -> Self {
self.virtual_view_display_lists = lists;
self
}
#[must_use] pub fn with_system_style(
mut self,
system_style: Option<std::sync::Arc<azul_css::system::SystemStyle>>,
) -> Self {
self.system_style = system_style;
self
}
#[must_use] pub fn from_gpu_cache(
gpu_cache: Option<&azul_core::gpu::GpuValueCache>,
dom_id: azul_core::dom::DomId,
scroll_offsets: &ScrollOffsetMap,
) -> Self {
let (transforms, opacities) = extract_gpu_values(gpu_cache, dom_id);
Self {
scroll_offsets: scroll_offsets.clone(),
transforms,
opacities,
system_style: None,
virtual_view_display_lists: std::collections::BTreeMap::new(),
image_callback_results: std::collections::BTreeMap::new(),
}
}
}
#[must_use] pub fn extract_gpu_values(
gpu_cache: Option<&azul_core::gpu::GpuValueCache>,
dom_id: azul_core::dom::DomId,
) -> (
HashMap<usize, azul_core::transform::ComputedTransform3D>,
HashMap<usize, f32>,
) {
{
let mut transforms = HashMap::new();
let mut opacities = HashMap::new();
if let Some(cache) = gpu_cache {
for (node_id, key) in &cache.transform_keys {
if let Some(value) = cache.current_transform_values.get(node_id) {
transforms.insert(key.id, *value);
}
}
for (node_id, key) in &cache.h_transform_keys {
if let Some(value) = cache.h_current_transform_values.get(node_id) {
transforms.insert(key.id, *value);
}
}
for (node_id, key) in &cache.css_transform_keys {
if let Some(value) = cache.css_current_transform_values.get(node_id) {
transforms.insert(key.id, *value);
}
}
for ((d, node_id), key) in &cache.scrollbar_v_opacity_keys {
if *d == dom_id {
if let Some(&value) = cache.scrollbar_v_opacity_values.get(&(*d, *node_id)) {
opacities.insert(key.id, value);
}
}
}
for ((d, node_id), key) in &cache.scrollbar_h_opacity_keys {
if *d == dom_id {
if let Some(&value) = cache.scrollbar_h_opacity_values.get(&(*d, *node_id)) {
opacities.insert(key.id, value);
}
}
}
for (node_id, key) in &cache.opacity_keys {
if let Some(&value) = cache.current_opacity_values.get(node_id) {
opacities.insert(key.id, value);
}
}
}
(transforms, opacities)
}
}
fn render_display_list(
display_list: &DisplayList,
pixmap: &mut AzulPixmap,
dpi_factor: f32,
renderer_resources: &RendererResources,
font_manager: Option<&FontManager<FontRef>>,
glyph_cache: &mut GlyphCache,
) -> Result<(), String> {
let empty_state = CpuRenderState::new(ScrollOffsetMap::new());
render_display_list_with_state(
display_list,
pixmap,
dpi_factor,
renderer_resources,
font_manager,
glyph_cache,
&empty_state,
)
}
fn render_display_list_with_state(
display_list: &DisplayList,
pixmap: &mut AzulPixmap,
dpi_factor: f32,
renderer_resources: &RendererResources,
font_manager: Option<&FontManager<FontRef>>,
glyph_cache: &mut GlyphCache,
render_state: &CpuRenderState,
) -> Result<(), String> {
let mut transform_stack = vec![TransAffine::new()]; let mut clip_stack: Vec<Option<AzRect>> = vec![None];
let mut mask_stack: Vec<MaskEntry> = Vec::new();
let mut scroll_offset_stack: Vec<(f32, f32)> = vec![(0.0, 0.0)];
let mut text_shadow_stack: Vec<StyleBoxShadow> = Vec::new();
let _p_loop = crate::probe::Probe::span("raster_loop");
for item in &display_list.items {
let _p_item = crate::probe::Probe::span(probe_label_for_item(item));
render_single_item(
item,
pixmap,
dpi_factor,
renderer_resources,
font_manager,
glyph_cache,
&mut transform_stack,
&mut clip_stack,
&mut mask_stack,
&mut scroll_offset_stack,
&mut text_shadow_stack,
render_state,
)?;
}
Ok(())
}
#[inline]
const fn probe_label_for_item(item: &DisplayListItem) -> &'static str {
use crate::solver3::display_list::DisplayListItem as I;
match item {
I::Rect { .. } => "dl:rect",
I::SelectionRect { .. } => "dl:sel_rect",
I::CursorRect { .. } => "dl:cursor",
I::Border { .. } => "dl:border",
I::Text { .. } => "dl:text",
I::TextLayout { .. } => "dl:text_layout",
I::Image { .. } => "dl:image",
I::ScrollBar { .. } => "dl:scrollbar_raw",
I::ScrollBarStyled { .. } => "dl:scrollbar",
I::PushClip { .. } => "dl:push_clip",
I::PopClip => "dl:pop_clip",
I::PushScrollFrame { .. } => "dl:push_scroll",
I::PopScrollFrame => "dl:pop_scroll",
I::PushStackingContext { .. } => "dl:push_stack",
I::PopStackingContext => "dl:pop_stack",
I::PushReferenceFrame { .. } => "dl:push_ref",
I::PopReferenceFrame => "dl:pop_ref",
I::PushOpacity { .. } => "dl:push_opacity",
I::PopOpacity => "dl:pop_opacity",
I::PushFilter { .. } => "dl:push_filter",
I::PopFilter => "dl:pop_filter",
I::PushBackdropFilter { .. } => "dl:push_bdfilter",
I::PopBackdropFilter => "dl:pop_bdfilter",
I::PushTextShadow { .. } => "dl:push_tshadow",
I::PopTextShadow => "dl:pop_tshadow",
I::PushImageMaskClip { .. } => "dl:push_imask",
I::PopImageMaskClip => "dl:pop_imask",
I::LinearGradient { .. } => "dl:linear_grad",
I::RadialGradient { .. } => "dl:radial_grad",
I::ConicGradient { .. } => "dl:conic_grad",
I::BoxShadow { .. } => "dl:box_shadow",
I::Underline { .. } => "dl:underline",
I::Strikethrough { .. } => "dl:strike",
I::Overline { .. } => "dl:overline",
I::HitTestArea { .. } => "dl:hit",
I::VirtualView { .. } => "dl:vview",
I::VirtualViewPlaceholder { .. } => "dl:vview_ph",
}
}
#[allow(clippy::cast_possible_truncation)] #[allow(clippy::similar_names)] #[allow(clippy::cast_possible_wrap, clippy::cast_precision_loss)] #[allow(clippy::too_many_lines)] pub fn render_display_list_damaged(
display_list: &DisplayList,
pixmap: &mut AzulPixmap,
dpi_factor: f32,
renderer_resources: &RendererResources,
font_manager: Option<&FontManager<FontRef>>,
glyph_cache: &mut GlyphCache,
render_state: &CpuRenderState,
damage_rects: &[LogicalRect],
) -> Result<(), String> {
struct SnappedRect {
x0: i32,
y0: i32,
x1: i32,
y1: i32,
logical: LogicalRect,
}
if damage_rects.is_empty() {
return Ok(()); }
let pw_i = pixmap.width() as i32;
let ph_i = pixmap.height() as i32;
let snap_out = |dr: &LogicalRect| -> Option<SnappedRect> {
let x0 = ((dr.origin.x * dpi_factor).floor() as i32).clamp(0, pw_i);
let y0 = ((dr.origin.y * dpi_factor).floor() as i32).clamp(0, ph_i);
let x1 = (((dr.origin.x + dr.size.width) * dpi_factor).ceil() as i32).clamp(0, pw_i);
let y1 = (((dr.origin.y + dr.size.height) * dpi_factor).ceil() as i32).clamp(0, ph_i);
if x1 <= x0 || y1 <= y0 {
return None;
}
Some(SnappedRect {
x0,
y0,
x1,
y1,
logical: LogicalRect {
origin: LogicalPosition {
x: x0 as f32 / dpi_factor,
y: y0 as f32 / dpi_factor,
},
size: LogicalSize {
width: (x1 - x0) as f32 / dpi_factor,
height: (y1 - y0) as f32 / dpi_factor,
},
},
})
};
let mut rects: Vec<SnappedRect> = damage_rects.iter().filter_map(snap_out).collect();
let mut i = 0;
while i < rects.len() {
let mut j = i + 1;
let mut merged_any = false;
while j < rects.len() {
let (a, b) = (&rects[i], &rects[j]);
let overlap = a.x0 < b.x1 && b.x0 < a.x1 && a.y0 < b.y1 && b.y0 < a.y1;
if overlap {
let x0 = a.x0.min(b.x0);
let y0 = a.y0.min(b.y0);
let x1 = a.x1.max(b.x1);
let y1 = a.y1.max(b.y1);
rects[i] = SnappedRect {
x0,
y0,
x1,
y1,
logical: LogicalRect {
origin: LogicalPosition {
x: x0 as f32 / dpi_factor,
y: y0 as f32 / dpi_factor,
},
size: LogicalSize {
width: (x1 - x0) as f32 / dpi_factor,
height: (y1 - y0) as f32 / dpi_factor,
},
},
};
rects.swap_remove(j);
merged_any = true;
} else {
j += 1;
}
}
if merged_any {
if rects.len() > 1 {
continue;
}
}
i += 1;
}
for sr in &rects {
pixmap.fill_rect(
sr.x0,
sr.y0,
sr.x1 - sr.x0,
sr.y1 - sr.y0,
255,
255,
255,
255,
);
let base_clip = AzRect::from_xywh(
sr.x0 as f32,
sr.y0 as f32,
(sr.x1 - sr.x0) as f32,
(sr.y1 - sr.y0) as f32,
);
let mut transform_stack = vec![TransAffine::new()];
let mut clip_stack: Vec<Option<AzRect>> = vec![base_clip];
let mut mask_stack: Vec<MaskEntry> = Vec::new();
let mut scroll_offset_stack: Vec<(f32, f32)> = vec![(0.0, 0.0)];
let mut text_shadow_stack: Vec<StyleBoxShadow> = Vec::new();
for item in &display_list.items {
if !item.is_state_management() {
if let Some(item_bounds) = item.bounds() {
let (sdx, sdy) = *scroll_offset_stack.last().unwrap_or(&(0.0, 0.0));
let test_bounds = if sdx == 0.0 && sdy == 0.0 {
item_bounds
} else {
LogicalRect {
origin: LogicalPosition {
x: item_bounds.origin.x - sdx,
y: item_bounds.origin.y - sdy,
},
size: item_bounds.size,
}
};
if !rects_overlap_or_adjacent(&test_bounds, &sr.logical, 0.0) {
continue;
}
}
}
render_single_item(
item,
pixmap,
dpi_factor,
renderer_resources,
font_manager,
glyph_cache,
&mut transform_stack,
&mut clip_stack,
&mut mask_stack,
&mut scroll_offset_stack,
&mut text_shadow_stack,
render_state,
)?;
}
}
Ok(())
}
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_sign_loss)] #[allow(clippy::similar_names)] #[allow(clippy::float_cmp)] #[allow(clippy::match_same_arms)] #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] pub fn render_single_item(
item: &DisplayListItem,
pixmap: &mut AzulPixmap,
dpi_factor: f32,
renderer_resources: &RendererResources,
font_manager: Option<&FontManager<FontRef>>,
glyph_cache: &mut GlyphCache,
transform_stack: &mut Vec<TransAffine>,
clip_stack: &mut Vec<Option<AzRect>>,
mask_stack: &mut Vec<MaskEntry>,
scroll_offset_stack: &mut Vec<(f32, f32)>,
text_shadow_stack: &mut Vec<StyleBoxShadow>,
render_state: &CpuRenderState,
) -> Result<(), String> {
use azul_css::props::style::border::BorderStyle;
let (scroll_dx, scroll_dy) = *scroll_offset_stack.last().unwrap_or(&(0.0, 0.0));
let scroll_rect = |r: &LogicalRect| -> LogicalRect {
if scroll_dx == 0.0 && scroll_dy == 0.0 {
return *r;
}
LogicalRect {
origin: LogicalPosition {
x: r.origin.x - scroll_dx,
y: r.origin.y - scroll_dy,
},
size: r.size,
}
};
match item {
DisplayListItem::Rect {
bounds,
color,
border_radius,
} => {
let clip = *clip_stack.last().unwrap();
render_rect(
pixmap,
&scroll_rect(bounds.inner()),
*color,
border_radius,
clip,
dpi_factor,
);
}
DisplayListItem::SelectionRect {
bounds,
color,
border_radius,
} => {
let clip = *clip_stack.last().unwrap();
render_rect(
pixmap,
&scroll_rect(bounds.inner()),
*color,
border_radius,
clip,
dpi_factor,
);
}
DisplayListItem::CursorRect { bounds, color } => {
let clip = *clip_stack.last().unwrap();
render_rect(
pixmap,
&scroll_rect(bounds.inner()),
*color,
&BorderRadius::default(),
clip,
dpi_factor,
);
}
DisplayListItem::Border {
bounds,
widths,
colors,
styles,
border_radius,
} => {
let default_color = ColorU {
r: 0,
g: 0,
b: 0,
a: 255,
};
let w_top = widths
.top
.and_then(|w| w.get_property().copied())
.map_or(0.0, |w| {
w.inner
.to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)
});
let w_right = widths
.right
.and_then(|w| w.get_property().copied())
.map_or(0.0, |w| {
w.inner
.to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)
});
let w_bottom = widths
.bottom
.and_then(|w| w.get_property().copied())
.map_or(0.0, |w| {
w.inner
.to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)
});
let w_left = widths
.left
.and_then(|w| w.get_property().copied())
.map_or(0.0, |w| {
w.inner
.to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)
});
let c_top = colors
.top
.and_then(|c| c.get_property().copied())
.map_or(default_color, |c| c.inner);
let c_right = colors
.right
.and_then(|c| c.get_property().copied())
.map_or(default_color, |c| c.inner);
let c_bottom = colors
.bottom
.and_then(|c| c.get_property().copied())
.map_or(default_color, |c| c.inner);
let c_left = colors
.left
.and_then(|c| c.get_property().copied())
.map_or(default_color, |c| c.inner);
let s_top = styles
.top
.and_then(|s| s.get_property().copied())
.map_or(BorderStyle::Solid, |s| s.inner);
let s_right = styles
.right
.and_then(|s| s.get_property().copied())
.map_or(BorderStyle::Solid, |s| s.inner);
let s_bottom = styles
.bottom
.and_then(|s| s.get_property().copied())
.map_or(BorderStyle::Solid, |s| s.inner);
let s_left = styles
.left
.and_then(|s| s.get_property().copied())
.map_or(BorderStyle::Solid, |s| s.inner);
let simple_radius = BorderRadius {
top_left: border_radius.top_left.to_pixels_internal(
bounds.0.size.width,
DEFAULT_FONT_SIZE,
DEFAULT_FONT_SIZE,
),
top_right: border_radius.top_right.to_pixels_internal(
bounds.0.size.width,
DEFAULT_FONT_SIZE,
DEFAULT_FONT_SIZE,
),
bottom_left: border_radius.bottom_left.to_pixels_internal(
bounds.0.size.width,
DEFAULT_FONT_SIZE,
DEFAULT_FONT_SIZE,
),
bottom_right: border_radius.bottom_right.to_pixels_internal(
bounds.0.size.width,
DEFAULT_FONT_SIZE,
DEFAULT_FONT_SIZE,
),
};
let clip = *clip_stack.last().unwrap();
let b = scroll_rect(bounds.inner());
let all_same = c_top == c_right
&& c_top == c_bottom
&& c_top == c_left
&& w_top == w_right
&& w_top == w_bottom
&& w_top == w_left
&& s_top == s_right
&& s_top == s_bottom
&& s_top == s_left;
if all_same {
render_border(
pixmap,
&b,
c_top,
w_top,
s_top,
&simple_radius,
clip,
dpi_factor,
);
} else {
render_border_sides(
pixmap,
&b,
[c_top, c_right, c_bottom, c_left],
[w_top, w_right, w_bottom, w_left],
[s_top, s_right, s_bottom, s_left],
&simple_radius,
clip,
dpi_factor,
);
}
}
DisplayListItem::Underline {
bounds,
color,
thickness: _,
} => {
let clip = *clip_stack.last().unwrap();
render_rect(
pixmap,
&scroll_rect(bounds.inner()),
*color,
&BorderRadius::default(),
clip,
dpi_factor,
);
}
DisplayListItem::Strikethrough {
bounds,
color,
thickness: _,
} => {
let clip = *clip_stack.last().unwrap();
render_rect(
pixmap,
&scroll_rect(bounds.inner()),
*color,
&BorderRadius::default(),
clip,
dpi_factor,
);
}
DisplayListItem::Overline {
bounds,
color,
thickness: _,
} => {
let clip = *clip_stack.last().unwrap();
render_rect(
pixmap,
&scroll_rect(bounds.inner()),
*color,
&BorderRadius::default(),
clip,
dpi_factor,
);
}
DisplayListItem::Text {
glyphs,
font_size_px,
font_hash,
color,
clip_rect,
..
} => {
let clip = *clip_stack.last().unwrap();
let text_clip = scroll_rect(clip_rect.inner());
for shadow in text_shadow_stack.iter() {
render_text_shadow(
shadow,
glyphs,
*font_hash,
*font_size_px,
pixmap,
&text_clip,
clip,
renderer_resources,
font_manager,
dpi_factor,
glyph_cache,
(scroll_dx, scroll_dy),
);
}
render_text(
glyphs,
*font_hash,
*font_size_px,
*color,
pixmap,
&text_clip,
clip,
renderer_resources,
font_manager,
dpi_factor,
glyph_cache,
(scroll_dx, scroll_dy),
false,
);
}
DisplayListItem::TextLayout {
layout,
bounds,
font_hash,
font_size_px,
color,
} => {
}
DisplayListItem::Image { bounds, image, .. } => {
let clip = *clip_stack.last().unwrap();
let resolved = render_state.image_callback_results.get(&image.get_hash());
render_image(
pixmap,
&scroll_rect(bounds.inner()),
resolved.unwrap_or(image),
clip,
dpi_factor,
);
}
DisplayListItem::ScrollBar {
bounds,
color,
orientation,
opacity_key: _,
hit_id: _,
} => {
let clip = *clip_stack.last().unwrap();
render_rect(
pixmap,
&scroll_rect(bounds.inner()),
*color,
&BorderRadius::default(),
clip,
dpi_factor,
);
}
DisplayListItem::ScrollBarStyled { info } => {
let clip = *clip_stack.last().unwrap();
let scrollbar_opacity = info
.opacity_key
.and_then(|key| render_state.opacities.get(&key.id).copied())
.unwrap_or(1.0);
if scrollbar_opacity > 0.001 {
if info.track_color.a > 0 {
render_rect(
pixmap,
&scroll_rect(info.track_bounds.inner()),
info.track_color,
&BorderRadius::default(),
clip,
dpi_factor,
);
}
if let Some(btn_bounds) = &info.button_decrement_bounds {
if info.button_color.a > 0 {
render_rect(
pixmap,
&scroll_rect(btn_bounds.inner()),
info.button_color,
&BorderRadius::default(),
clip,
dpi_factor,
);
}
}
if let Some(btn_bounds) = &info.button_increment_bounds {
if info.button_color.a > 0 {
render_rect(
pixmap,
&scroll_rect(btn_bounds.inner()),
info.button_color,
&BorderRadius::default(),
clip,
dpi_factor,
);
}
}
if info.thumb_color.a > 0 {
let thumb_rect = info.thumb_bounds.inner();
let transform = info
.thumb_transform_key
.and_then(|key| render_state.transforms.get(&key.id))
.unwrap_or(&info.thumb_initial_transform);
let tx = transform.m[3][0];
let ty = transform.m[3][1];
let transformed_thumb = LogicalRect {
origin: LogicalPosition {
x: thumb_rect.origin.x + tx,
y: thumb_rect.origin.y + ty,
},
size: thumb_rect.size,
};
render_rect(
pixmap,
&scroll_rect(&transformed_thumb),
info.thumb_color,
&info.thumb_border_radius,
clip,
dpi_factor,
);
}
} }
DisplayListItem::PushClip {
bounds,
border_radius,
} => {
let new_clip = logical_rect_to_az_rect(&scroll_rect(bounds.inner()), dpi_factor);
let merged = intersect_clips(clip_stack.last().copied().flatten(), new_clip);
clip_stack.push(merged);
}
DisplayListItem::PopClip => {
if clip_stack.len() > 1 {
clip_stack.pop();
} else {
#[cfg(feature = "std")]
if std::env::var("AZ_CLIP_DEBUG").is_ok() {
eprintln!(
"[CpuBackend] PopClip with no matching PushClip — clamping to base clip"
);
}
}
}
DisplayListItem::PushScrollFrame { scroll_id, .. } => {
transform_stack.push(
transform_stack
.last()
.copied()
.unwrap_or_else(TransAffine::new),
);
let frame_offset = render_state
.scroll_offsets
.get(scroll_id)
.copied()
.unwrap_or((0.0, 0.0));
let new_scroll = (scroll_dx + frame_offset.0, scroll_dy + frame_offset.1);
scroll_offset_stack.push(new_scroll);
}
DisplayListItem::PopScrollFrame => {
if transform_stack.len() > 1 {
transform_stack.pop();
}
if scroll_offset_stack.len() > 1 {
scroll_offset_stack.pop();
}
}
DisplayListItem::HitTestArea { bounds, tag } => {
}
DisplayListItem::PushStackingContext { z_index, bounds } => {
}
DisplayListItem::PopStackingContext => {}
DisplayListItem::VirtualView {
child_dom_id,
bounds,
clip_rect,
} => {
let _ = clip_rect;
let child_dl = render_state.virtual_view_display_lists.get(child_dom_id).cloned();
#[cfg(feature = "std")]
if std::env::var("AZ_MAP_DEBUG").is_ok() {
eprintln!(
"[cpu-vview] VirtualView item: child_dom_id={} found={} items={} bounds={:?} avail_ids={:?}",
child_dom_id.inner,
child_dl.is_some(),
child_dl.as_ref().map_or(0, |d| d.items.len()),
bounds.inner(),
render_state.virtual_view_display_lists.keys().map(|k| k.inner).collect::<Vec<_>>(),
);
}
if let Some(child_dl) = child_dl {
let vv_origin = bounds.inner().origin;
let vv_clip = intersect_clips(
clip_stack.last().copied().flatten(),
logical_rect_to_az_rect(&scroll_rect(bounds.inner()), dpi_factor),
);
clip_stack.push(vv_clip);
scroll_offset_stack.push((scroll_dx - vv_origin.x, scroll_dy - vv_origin.y));
for child_item in &child_dl.items {
render_single_item(
child_item,
pixmap,
dpi_factor,
renderer_resources,
font_manager,
glyph_cache,
transform_stack,
clip_stack,
mask_stack,
scroll_offset_stack,
text_shadow_stack,
render_state,
)?;
}
scroll_offset_stack.pop();
clip_stack.pop();
}
}
DisplayListItem::VirtualViewPlaceholder { .. } => {
#[cfg(feature = "std")]
if std::env::var("AZ_MAP_DEBUG").is_ok() {
eprintln!("[cpu-vview] VirtualViewPlaceholder hit (NOT swapped to a VirtualView item — nothing composites)");
}
}
DisplayListItem::LinearGradient {
bounds,
gradient,
border_radius,
} => {
let clip = *clip_stack.last().unwrap();
render_linear_gradient(
pixmap,
&scroll_rect(bounds.inner()),
gradient,
border_radius,
clip,
dpi_factor,
render_state.system_style.as_deref().map(|s| &s.colors),
);
}
DisplayListItem::RadialGradient {
bounds,
gradient,
border_radius,
} => {
let clip = *clip_stack.last().unwrap();
render_radial_gradient(
pixmap,
&scroll_rect(bounds.inner()),
gradient,
border_radius,
clip,
dpi_factor,
render_state.system_style.as_deref().map(|s| &s.colors),
);
}
DisplayListItem::ConicGradient {
bounds,
gradient,
border_radius,
} => {
let clip = *clip_stack.last().unwrap();
render_conic_gradient(
pixmap,
&scroll_rect(bounds.inner()),
gradient,
border_radius,
clip,
dpi_factor,
render_state.system_style.as_deref().map(|s| &s.colors),
);
}
DisplayListItem::BoxShadow {
bounds,
shadow,
border_radius,
} => {
render_box_shadow(
pixmap,
&scroll_rect(bounds.inner()),
shadow,
border_radius,
dpi_factor,
)?;
}
DisplayListItem::PushOpacity { bounds, opacity } => {
let rect = logical_rect_to_az_rect(&scroll_rect(bounds.inner()), dpi_factor);
if let Some(r) = rect {
let snap = snapshot_region(
pixmap,
r.x as i32,
r.y as i32,
r.width as u32,
r.height as u32,
);
mask_stack.push(MaskEntry::Opacity {
snapshot: snap,
rect: r,
opacity: *opacity,
});
}
}
DisplayListItem::PopOpacity => {
if let Some(MaskEntry::Opacity {
snapshot,
rect,
opacity,
}) = mask_stack.pop()
{
let x = rect.x as i32;
let y = rect.y as i32;
let w = rect.width as u32;
let h = rect.height as u32;
let pw = pixmap.width as i32;
let ph = pixmap.height as i32;
for py in 0..h as i32 {
let dy = y + py;
if dy < 0 || dy >= ph {
continue;
}
for px in 0..w as i32 {
let dx = x + px;
if dx < 0 || dx >= pw {
continue;
}
let pi = ((dy as u32 * pixmap.width + dx as u32) * 4) as usize;
let si = ((py as u32 * w + px as u32) * 4) as usize;
if pi + 3 >= pixmap.data.len() || si + 3 >= snapshot.len() {
continue;
}
let op = (opacity * 255.0).clamp(0.0, 255.0) as u32;
let inv_op = 255 - op;
for c in 0..4 {
let snap_c = u32::from(snapshot[si + c]);
let cur_c = u32::from(pixmap.data[pi + c]);
pixmap.data[pi + c] = ((cur_c * op + snap_c * inv_op) / 255) as u8;
}
}
}
}
}
DisplayListItem::PushReferenceFrame {
transform_key,
initial_transform,
bounds,
} => {
let live_transform = render_state.transforms.get(&transform_key.id);
let m = live_transform.map_or(&initial_transform.m, |t| &t.m);
let tf = TransAffine::new_custom(
f64::from(m[0][0]),
f64::from(m[0][1]), f64::from(m[1][0]),
f64::from(m[1][1]), f64::from(m[3][0]),
f64::from(m[3][1]), );
let current = transform_stack
.last()
.copied()
.unwrap_or_else(TransAffine::new);
let mut composed = tf;
composed.premultiply(¤t);
transform_stack.push(composed);
}
DisplayListItem::PopReferenceFrame => {
if transform_stack.len() > 1 {
transform_stack.pop();
}
}
DisplayListItem::PushFilter { .. } => {}
DisplayListItem::PopFilter => {}
DisplayListItem::PushBackdropFilter { .. } => {}
DisplayListItem::PopBackdropFilter => {}
DisplayListItem::PushTextShadow { shadow } => {
text_shadow_stack.push(*shadow);
}
DisplayListItem::PopTextShadow => {
text_shadow_stack.pop();
}
DisplayListItem::PushImageMaskClip {
bounds,
mask_image,
mask_rect,
} => {
let mr = &scroll_rect(mask_rect.inner());
let px_x = (mr.origin.x * dpi_factor) as i32;
let px_y = (mr.origin.y * dpi_factor) as i32;
let px_w = (mr.size.width * dpi_factor).ceil() as u32;
let px_h = (mr.size.height * dpi_factor).ceil() as u32;
if px_w > 0 && px_h > 0 {
let snapshot = snapshot_region(pixmap, px_x, px_y, px_w, px_h);
let mask_data = extract_mask_data(mask_image, px_w, px_h)
.unwrap_or_else(|| vec![255u8; (px_w * px_h) as usize]);
mask_stack.push(MaskEntry::ImageMask {
snapshot,
mask_data,
origin_x: px_x,
origin_y: px_y,
width: px_w,
height: px_h,
});
}
}
DisplayListItem::PopImageMaskClip => {
if let Some(entry) = mask_stack.pop() {
apply_mask(pixmap, &entry);
}
}
}
Ok(())
}
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] fn render_rect(
pixmap: &mut AzulPixmap,
bounds: &LogicalRect,
color: ColorU,
border_radius: &BorderRadius,
clip: Option<AzRect>,
dpi_factor: f32,
) {
if color.a == 0 {
return;
}
let Some(rect) = logical_rect_to_az_rect(bounds, dpi_factor) else {
return;
};
if let Some(ref c) = clip {
if rect.clip(c).is_none() {
return;
}
}
let agg_color = Rgba8::new(
u32::from(color.r),
u32::from(color.g),
u32::from(color.b),
u32::from(color.a),
);
if border_radius.is_zero() {
let w = pixmap.width;
let h = pixmap.height;
let stride = (w * 4) as i32;
let mut ra = unsafe { RowAccessor::new_with_buf(pixmap.data.as_mut_ptr(), w, h, stride) };
let mut pf = PixfmtRgba32::new(&mut ra);
let mut rb = RendererBase::new(pf);
if let Some(c) = clip {
rb.clip_box_i(
c.x as i32,
c.y as i32,
(c.x + c.width) as i32 - 1,
(c.y + c.height) as i32 - 1,
);
}
rb.blend_bar(
rect.x as i32,
rect.y as i32,
(rect.x + rect.width) as i32 - 1,
(rect.y + rect.height) as i32 - 1,
&agg_color,
255, );
} else {
let mut path = build_rounded_rect_path(&rect, border_radius, dpi_factor);
agg_fill_path_clipped(pixmap, &mut path, &agg_color, FillingRule::NonZero, clip);
}
}
pub const TEXT_LCD_DEFAULT: bool = true;
fn text_lcd_enabled() -> bool {
static V: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*V.get_or_init(|| {
std::env::var("AZ_TEXT_LCD")
.map(|s| !(s == "0" || s.eq_ignore_ascii_case("false")))
.unwrap_or(TEXT_LCD_DEFAULT)
})
}
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_sign_loss)] #[allow(clippy::too_many_arguments)] fn render_glyphs_lcd(
pixmap: &mut AzulPixmap,
clip: Option<AzRect>,
glyphs: &[GlyphInstance],
parsed_font: &ParsedFont,
font_hash: FontHash,
ppem: u16,
scale: f32,
hint_correction: f32,
color: ColorU,
dpi_factor: f32,
scroll_offset: (f32, f32),
glyph_cache: &mut GlyphCache,
) {
use agg_rust::pixfmt_lcd::{LcdDistributionLut, PixfmtRgba32Lcd};
let agg_color = Rgba8::new(
u32::from(color.r),
u32::from(color.g),
u32::from(color.b),
u32::from(color.a),
);
let subpx = crate::glyph_cache::text_subpixel_enabled();
let mut ras = RasterizerScanlineAa::new();
ras.filling_rule(FillingRule::NonZero);
for glyph in glyphs {
let glyph_index = glyph.index as u16;
let Some(glyph_data) = parsed_font.get_or_decode_glyph(glyph_index) else {
continue;
};
let Some(cached) = glyph_cache.get_or_build(
font_hash.font_hash,
glyph_index,
&glyph_data,
parsed_font,
ppem,
) else {
continue;
};
let is_hinted = cached.is_hinted;
let glyph_x = (glyph.point.x - scroll_offset.0) * dpi_factor;
let glyph_baseline_y = (glyph.point.y - scroll_offset.1) * dpi_factor;
let px = if subpx { glyph_x } else { glyph_x.round() };
let py = glyph_baseline_y.round();
let rescale_hinted = is_hinted && (hint_correction - 1.0).abs() > 1e-4;
let path_scale = if is_hinted {
if rescale_hinted { f64::from(hint_correction) } else { 1.0 }
} else {
f64::from(scale)
};
let mut transform = TransAffine::new_scaling(3.0 * path_scale, path_scale);
transform.multiply(&TransAffine::new_translation(3.0 * f64::from(px), f64::from(py)));
ras.add_path_vertices_transformed(cached.path.vertices(), &transform);
}
let w = pixmap.width;
let h = pixmap.height;
let stride = (w * 4) as i32;
let mut ra = unsafe { RowAccessor::new_with_buf(pixmap.data.as_mut_ptr(), w, h, stride) };
let lut = LcdDistributionLut::new(f64::from(0x56u32), f64::from(0x4Du32), f64::from(0x08u32));
let pf = PixfmtRgba32Lcd::new(&mut ra, &lut);
let mut rb = RendererBase::new(pf);
if let Some(c) = clip {
rb.clip_box_i(
(c.x as i32) * 3,
c.y as i32,
((c.x + c.width) as i32) * 3 - 1,
(c.y + c.height) as i32 - 1,
);
}
let mut sl = ScanlineU8::new();
render_scanlines_aa_solid(&mut ras, &mut sl, &mut rb, &agg_color);
}
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_sign_loss)] #[allow(clippy::too_many_lines)] fn render_text(
glyphs: &[GlyphInstance],
font_hash: FontHash,
font_size_px: f32,
color: ColorU,
pixmap: &mut AzulPixmap,
clip_rect: &LogicalRect,
clip: Option<AzRect>,
renderer_resources: &RendererResources,
font_manager: Option<&FontManager<FontRef>>,
dpi_factor: f32,
glyph_cache: &mut GlyphCache,
scroll_offset: (f32, f32),
force_grayscale: bool,
) {
if color.a == 0 || glyphs.is_empty() {
return;
}
if let Some(ref c) = clip {
let Some(text_rect) = logical_rect_to_az_rect(clip_rect, dpi_factor) else {
return;
};
if text_rect.clip(c).is_none() {
return; }
}
let agg_color = Rgba8::new(
u32::from(color.r),
u32::from(color.g),
u32::from(color.b),
u32::from(color.a),
);
let parsed_font: &ParsedFont = if let Some(fm) = font_manager {
if let Some(font_ref) = fm.get_font_by_hash(font_hash.font_hash) { unsafe { &*font_ref.get_parsed().cast::<ParsedFont>() } } else {
eprintln!(
"[cpurender] Font hash {} not found in FontManager",
font_hash.font_hash
);
return;
}
} else {
let Some(font_key) = renderer_resources.font_hash_map.get(&font_hash.font_hash) else {
eprintln!(
"[cpurender] Font hash {} not found in font_hash_map (available: {:?})",
font_hash.font_hash,
renderer_resources.font_hash_map.keys().collect::<Vec<_>>()
);
return;
};
let Some((font_ref, _instances)) = renderer_resources.currently_registered_fonts.get(font_key) else {
eprintln!(
"[cpurender] FontKey {font_key:?} not found in currently_registered_fonts"
);
return;
};
unsafe { &*font_ref.get_parsed().cast::<ParsedFont>() }
};
let units_per_em = f32::from(parsed_font.font_metrics.units_per_em);
if units_per_em <= 0.0 {
return;
}
let effective_px = font_size_px * dpi_factor;
let scale = effective_px / units_per_em;
let ppem = effective_px.round() as u16;
let hint_correction = if ppem > 0 { effective_px / f32::from(ppem) } else { 1.0 };
if text_lcd_enabled() && !force_grayscale {
render_glyphs_lcd(
pixmap, clip, glyphs, parsed_font, font_hash, ppem, scale,
hint_correction, color, dpi_factor, scroll_offset, glyph_cache,
);
return;
}
let w = pixmap.width;
let h = pixmap.height;
let stride = (w * 4) as i32;
let mut ra = unsafe { RowAccessor::new_with_buf(pixmap.data.as_mut_ptr(), w, h, stride) };
let mut pf = PixfmtRgba32::new(&mut ra);
let mut rb = RendererBase::new(pf);
if let Some(c) = clip {
rb.clip_box_i(
c.x as i32,
c.y as i32,
(c.x + c.width) as i32 - 1,
(c.y + c.height) as i32 - 1,
);
}
let mut ras = RasterizerScanlineAa::new();
ras.filling_rule(FillingRule::NonZero);
for glyph in glyphs {
let glyph_index = glyph.index as u16;
let Some(glyph_data) = parsed_font.get_or_decode_glyph(glyph_index) else {
continue;
};
let is_hinted = glyph_cache
.get_or_build(
font_hash.font_hash,
glyph_index,
&glyph_data,
parsed_font,
ppem,
)
.is_some_and(|c| c.is_hinted);
let glyph_x = (glyph.point.x - scroll_offset.0) * dpi_factor;
let glyph_baseline_y = (glyph.point.y - scroll_offset.1) * dpi_factor;
let Some((cells, int_x, int_y)) = glyph_cache.get_or_build_cells(
font_hash.font_hash,
glyph_index,
ppem,
glyph_x,
glyph_baseline_y,
scale,
is_hinted,
hint_correction,
) else {
continue;
};
ras.add_cells_offset(cells, int_x, int_y);
}
let mut sl = ScanlineU8::new();
render_scanlines_aa_solid(&mut ras, &mut sl, &mut rb, &agg_color);
}
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_sign_loss)]
fn render_text_shadow(
shadow: &StyleBoxShadow,
glyphs: &[GlyphInstance],
font_hash: FontHash,
font_size_px: f32,
pixmap: &mut AzulPixmap,
clip_rect: &LogicalRect,
clip: Option<AzRect>,
renderer_resources: &RendererResources,
font_manager: Option<&FontManager<FontRef>>,
dpi_factor: f32,
glyph_cache: &mut GlyphCache,
scroll_offset: (f32, f32),
) {
let color = shadow.color;
if color.a == 0 || glyphs.is_empty() {
return;
}
let off_x = shadow
.offset_x
.inner
.to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE);
let off_y = shadow
.offset_y
.inner
.to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE);
let blur_logical = shadow
.blur_radius
.inner
.to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)
.max(0.0);
let Some(mut tmp) = AzulPixmap::new(pixmap.width, pixmap.height) else {
return;
};
tmp.fill(0, 0, 0, 0);
let shifted: Vec<GlyphInstance> = glyphs
.iter()
.map(|g| {
let mut g = *g;
g.point.x += off_x;
g.point.y += off_y;
g
})
.collect();
let shadow_clip_rect = LogicalRect {
origin: LogicalPosition {
x: clip_rect.origin.x + off_x,
y: clip_rect.origin.y + off_y,
},
size: clip_rect.size,
};
render_text(
&shifted,
font_hash,
font_size_px,
color,
&mut tmp,
&shadow_clip_rect,
clip,
renderer_resources,
font_manager,
dpi_factor,
glyph_cache,
scroll_offset,
true,
);
let blur_px = blur_logical * dpi_factor;
if blur_px > 0.5 {
let radius = (blur_px.ceil() as u32).min(254);
let w = tmp.width;
let h = tmp.height;
let stride = (w * 4) as i32;
let mut ra = unsafe { RowAccessor::new_with_buf(tmp.data.as_mut_ptr(), w, h, stride) };
stack_blur_rgba32(&mut ra, radius, radius);
}
blit_buffer(pixmap, &tmp.data, tmp.width, tmp.height, 0, 0);
}
#[allow(clippy::suboptimal_flops)] #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] fn render_border(
pixmap: &mut AzulPixmap,
bounds: &LogicalRect,
color: ColorU,
width: f32,
border_style: azul_css::props::style::border::BorderStyle,
border_radius: &BorderRadius,
clip: Option<AzRect>,
dpi_factor: f32,
) {
use azul_css::props::style::border::BorderStyle;
if color.a == 0 || width <= 0.0 {
return;
}
match border_style {
BorderStyle::None | BorderStyle::Hidden => return,
_ => {}
}
let Some(rect) = logical_rect_to_az_rect(bounds, dpi_factor) else {
return;
};
if let Some(ref c) = clip {
if rect.clip(c).is_none() {
return;
}
}
let scaled_width = width * dpi_factor;
let agg_color = Rgba8::new(
u32::from(color.r),
u32::from(color.g),
u32::from(color.b),
u32::from(color.a),
);
let mut path = build_rounded_rect_path(&rect, border_radius, dpi_factor);
let x = f64::from(rect.x);
let y = f64::from(rect.y);
let w = f64::from(rect.width);
let h = f64::from(rect.height);
let sw = f64::from(scaled_width);
let ir = AzRect::from_xywh(
rect.x + scaled_width,
rect.y + scaled_width,
rect.width - 2.0 * scaled_width,
rect.height - 2.0 * scaled_width,
);
if let Some(ir) = ir {
let inner_radius = BorderRadius {
top_left: (border_radius.top_left - width).max(0.0),
top_right: (border_radius.top_right - width).max(0.0),
bottom_right: (border_radius.bottom_right - width).max(0.0),
bottom_left: (border_radius.bottom_left - width).max(0.0),
};
let mut inner = build_rounded_rect_path(&ir, &inner_radius, dpi_factor);
path.concat_path(&mut inner, 0);
}
match border_style {
BorderStyle::Dashed | BorderStyle::Dotted => {
use agg_rust::conv_dash::ConvDash;
use agg_rust::conv_stroke::ConvStroke;
let half = sw / 2.0;
let mut stroke_path = PathStorage::new();
let (cx, cy, cw, ch) = (x + half, y + half, w - sw, h - sw);
stroke_path.move_to(cx, cy);
stroke_path.line_to(cx + cw, cy);
stroke_path.line_to(cx + cw, cy + ch);
stroke_path.line_to(cx, cy + ch);
stroke_path.close_polygon(PATH_FLAGS_NONE);
let mut dashed = ConvDash::new(stroke_path);
if border_style == BorderStyle::Dashed {
dashed.add_dash(sw * 3.0, sw);
} else {
dashed.add_dash(sw, sw);
}
let mut stroked = ConvStroke::new(dashed);
stroked.set_width(sw);
agg_fill_path_clipped(pixmap, &mut stroked, &agg_color, FillingRule::NonZero, clip);
}
_ if border_radius.is_zero() => {
let pw = pixmap.width;
let ph = pixmap.height;
let stride = (pw * 4) as i32;
let mut ra =
unsafe { RowAccessor::new_with_buf(pixmap.data.as_mut_ptr(), pw, ph, stride) };
let mut pf = PixfmtRgba32::new(&mut ra);
let mut rb = RendererBase::new(pf);
if let Some(c) = clip {
rb.clip_box_i(
c.x as i32,
c.y as i32,
(c.x + c.width) as i32 - 1,
(c.y + c.height) as i32 - 1,
);
}
let (xi, yi) = (x as i32, y as i32);
let (x2i, y2i) = ((x + w) as i32 - 1, (y + h) as i32 - 1);
let swi = sw as i32;
rb.blend_bar(xi, yi, x2i, yi + swi - 1, &agg_color, 255);
rb.blend_bar(xi, y2i - swi + 1, x2i, y2i, &agg_color, 255);
rb.blend_bar(xi, yi + swi, xi + swi - 1, y2i - swi, &agg_color, 255);
rb.blend_bar(x2i - swi + 1, yi + swi, x2i, y2i - swi, &agg_color, 255);
}
_ => {
agg_fill_path_clipped(pixmap, &mut path, &agg_color, FillingRule::EvenOdd, clip);
}
}
}
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] #[allow(clippy::too_many_lines)] fn render_border_sides(
pixmap: &mut AzulPixmap,
bounds: &LogicalRect,
colors: [ColorU; 4], widths: [f32; 4], _styles: [azul_css::props::style::border::BorderStyle; 4],
border_radius: &BorderRadius,
clip: Option<AzRect>,
dpi_factor: f32,
) {
let Some(rect) = logical_rect_to_az_rect(bounds, dpi_factor) else {
return;
};
let ox = f64::from(rect.x);
let oy = f64::from(rect.y);
let ow = f64::from(rect.width);
let oh = f64::from(rect.height);
let wt = f64::from(widths[0] * dpi_factor);
let wr = f64::from(widths[1] * dpi_factor);
let wb = f64::from(widths[2] * dpi_factor);
let wl = f64::from(widths[3] * dpi_factor);
let ix = ox + wl;
let iy = oy + wt;
let iw = ow - wl - wr;
let ih = oh - wt - wb;
let sides: [(f64, f64, f64, f64, f64, f64, f64, f64, ColorU, f32); 4] = [
(
ox,
oy,
ox + ow,
oy,
ix + iw,
iy,
ix,
iy,
colors[0],
widths[0],
),
(
ox + ow,
oy,
ox + ow,
oy + oh,
ix + iw,
iy + ih,
ix + iw,
iy,
colors[1],
widths[1],
),
(
ox + ow,
oy + oh,
ox,
oy + oh,
ix,
iy + ih,
ix + iw,
iy + ih,
colors[2],
widths[2],
),
(
ox,
oy + oh,
ox,
oy,
ix,
iy,
ix,
iy + ih,
colors[3],
widths[3],
),
];
if border_radius.is_zero() {
let pw = pixmap.width;
let ph = pixmap.height;
let stride = (pw * 4) as i32;
let mut ra = unsafe { RowAccessor::new_with_buf(pixmap.data.as_mut_ptr(), pw, ph, stride) };
let mut pf = PixfmtRgba32::new(&mut ra);
let mut rb = RendererBase::new(pf);
if let Some(c) = clip {
rb.clip_box_i(
c.x as i32,
c.y as i32,
(c.x + c.width) as i32 - 1,
(c.y + c.height) as i32 - 1,
);
}
if widths[0] > 0.0 && colors[0].a > 0 {
let c = colors[0];
let ac = Rgba8::new(u32::from(c.r), u32::from(c.g), u32::from(c.b), u32::from(c.a));
rb.blend_bar(
ox as i32,
oy as i32,
(ox + ow) as i32 - 1,
iy as i32 - 1,
&ac,
255,
);
}
if widths[2] > 0.0 && colors[2].a > 0 {
let c = colors[2];
let ac = Rgba8::new(u32::from(c.r), u32::from(c.g), u32::from(c.b), u32::from(c.a));
rb.blend_bar(
ox as i32,
(iy + ih) as i32,
(ox + ow) as i32 - 1,
(oy + oh) as i32 - 1,
&ac,
255,
);
}
if widths[3] > 0.0 && colors[3].a > 0 {
let c = colors[3];
let ac = Rgba8::new(u32::from(c.r), u32::from(c.g), u32::from(c.b), u32::from(c.a));
rb.blend_bar(
ox as i32,
iy as i32,
ix as i32 - 1,
(iy + ih) as i32 - 1,
&ac,
255,
);
}
if widths[1] > 0.0 && colors[1].a > 0 {
let c = colors[1];
let ac = Rgba8::new(u32::from(c.r), u32::from(c.g), u32::from(c.b), u32::from(c.a));
rb.blend_bar(
(ix + iw) as i32,
iy as i32,
(ox + ow) as i32 - 1,
(iy + ih) as i32 - 1,
&ac,
255,
);
}
} else {
for &(x0, y0, x1, y1, x2, y2, x3, y3, color, width) in &sides {
if width <= 0.0 || color.a == 0 {
continue;
}
let mut path = PathStorage::new();
path.move_to(x0, y0);
path.line_to(x1, y1);
path.line_to(x2, y2);
path.line_to(x3, y3);
path.close_polygon(PATH_FLAGS_NONE);
let agg_color = Rgba8::new(
u32::from(color.r),
u32::from(color.g),
u32::from(color.b),
u32::from(color.a),
);
agg_fill_path_clipped(pixmap, &mut path, &agg_color, FillingRule::NonZero, clip);
}
}
}
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_precision_loss, clippy::cast_sign_loss)] #[allow(clippy::many_single_char_names, clippy::similar_names)] #[allow(clippy::too_many_lines)] fn render_image(
pixmap: &mut AzulPixmap,
bounds: &LogicalRect,
image: &ImageRef,
clip: Option<AzRect>,
dpi_factor: f32,
) {
let Some(rect) = logical_rect_to_az_rect(bounds, dpi_factor) else {
return;
};
if let Some(ref c) = clip {
if rect.clip(c).is_none() {
return;
}
}
let image_data = image.get_data();
let (src_rgba, src_w, src_h) = match image_data {
DecodedImage::Raw((descriptor, data)) => {
let w = descriptor.width as u32;
let h = descriptor.height as u32;
if w == 0 || h == 0 {
return;
}
let bytes = match data {
azul_core::resources::ImageData::Raw(shared) => shared.as_ref(),
azul_core::resources::ImageData::External(_) => return,
};
let rgba = match descriptor.format {
azul_core::resources::RawImageFormat::RGBA8 => bytes.to_vec(),
azul_core::resources::RawImageFormat::RGB8 => {
let mut out = Vec::with_capacity(bytes.len() / 3 * 4);
for chunk in bytes.chunks_exact(3) {
out.extend_from_slice(&[chunk[0], chunk[1], chunk[2], 255]);
}
out
}
azul_core::resources::RawImageFormat::BGRA8 => {
let mut out = Vec::with_capacity(bytes.len());
for chunk in bytes.chunks_exact(4) {
let b = chunk[0];
let g = chunk[1];
let r = chunk[2];
let a = chunk[3];
out.push(r);
out.push(g);
out.push(b);
out.push(a);
}
out
}
azul_core::resources::RawImageFormat::R8 => {
let mut out = Vec::with_capacity(bytes.len() * 4);
for &v in bytes {
out.push(v);
out.push(v);
out.push(v);
out.push(v);
}
out
}
_ => {
let gray = Rgba8::new(200, 200, 200, 255);
let mut path = build_rect_path(&rect);
agg_fill_path(pixmap, &mut path, &gray, FillingRule::NonZero);
return;
}
};
(rgba, w, h)
}
DecodedImage::NullImage { .. } | DecodedImage::Callback(_) => {
let gray = Rgba8::new(200, 200, 200, 255);
let mut path = build_rect_path(&rect);
agg_fill_path(pixmap, &mut path, &gray, FillingRule::NonZero);
return;
}
DecodedImage::Gl(_) => return,
};
let dst_x = rect.x as i32;
let dst_y = rect.y as i32;
let dst_w = rect.width as u32;
let dst_h = rect.height as u32;
let pw = pixmap.width;
let ph = pixmap.height;
let sx = src_w as f32 / dst_w.max(1) as f32;
let sy = src_h as f32 / dst_h.max(1) as f32;
let (clip_x1, clip_y1, clip_x2, clip_y2) = clip.as_ref().map_or((0, 0, pw as i32, ph as i32), |c| (
c.x as i32,
c.y as i32,
(c.x + c.width) as i32,
(c.y + c.height) as i32,
));
for py in 0..dst_h {
for px in 0..dst_w {
let tx = dst_x + px as i32;
let ty = dst_y + py as i32;
if tx < 0 || ty < 0 || tx >= pw as i32 || ty >= ph as i32 {
continue;
}
if tx < clip_x1 || ty < clip_y1 || tx >= clip_x2 || ty >= clip_y2 {
continue;
}
let src_x = ((px as f32 * sx) as u32).min(src_w - 1);
let src_y = ((py as f32 * sy) as u32).min(src_h - 1);
let si = ((src_y * src_w + src_x) * 4) as usize;
let di = ((ty as u32 * pw + tx as u32) * 4) as usize;
if si + 3 < src_rgba.len() && di + 3 < pixmap.data.len() {
let sa = u32::from(src_rgba[si + 3]);
if sa == 255 {
pixmap.data[di] = src_rgba[si];
pixmap.data[di + 1] = src_rgba[si + 1];
pixmap.data[di + 2] = src_rgba[si + 2];
pixmap.data[di + 3] = 255;
} else if sa > 0 {
let da = 255 - sa;
pixmap.data[di] =
((u32::from(src_rgba[si]) * sa + u32::from(pixmap.data[di]) * da) / 255) as u8;
pixmap.data[di + 1] = ((u32::from(src_rgba[si + 1]) * sa
+ u32::from(pixmap.data[di + 1]) * da)
/ 255) as u8;
pixmap.data[di + 2] = ((u32::from(src_rgba[si + 2]) * sa
+ u32::from(pixmap.data[di + 2]) * da)
/ 255) as u8;
pixmap.data[di + 3] =
((sa + u32::from(pixmap.data[di + 3]) * da / 255).min(255)) as u8;
}
}
}
}
}
fn build_rect_path(rect: &AzRect) -> PathStorage {
let mut path = PathStorage::new();
let x = f64::from(rect.x);
let y = f64::from(rect.y);
let w = f64::from(rect.width);
let h = f64::from(rect.height);
path.move_to(x, y);
path.line_to(x + w, y);
path.line_to(x + w, y + h);
path.line_to(x, y + h);
path.close_polygon(PATH_FLAGS_NONE);
path
}
fn build_rounded_rect_path(
rect: &AzRect,
border_radius: &BorderRadius,
dpi_factor: f32,
) -> PathStorage {
let mut path = PathStorage::new();
let x = f64::from(rect.x);
let y = f64::from(rect.y);
let w = f64::from(rect.width);
let h = f64::from(rect.height);
let tl = f64::from(border_radius.top_left * dpi_factor);
let tr = f64::from(border_radius.top_right * dpi_factor);
let br = f64::from(border_radius.bottom_right * dpi_factor);
let bl = f64::from(border_radius.bottom_left * dpi_factor);
if tl <= 0.0 && tr <= 0.0 && br <= 0.0 && bl <= 0.0 {
path.move_to(x, y);
path.line_to(x + w, y);
path.line_to(x + w, y + h);
path.line_to(x, y + h);
path.close_polygon(PATH_FLAGS_NONE);
return path;
}
let mut rr = RoundedRect::default_new();
rr.rect(x, y, x + w, y + h);
rr.radius_all(tl, tl, tr, tr, br, br, bl, bl);
rr.normalize_radius();
rr.set_approximation_scale(f64::from(dpi_factor.max(1.0)));
path.concat_path(&mut rr, 0);
path
}
#[derive(Debug, Clone, Copy)]
pub struct ComponentPreviewOptions {
pub width: Option<f32>,
pub height: Option<f32>,
pub dpi_factor: f32,
pub background_color: ColorU,
}
impl Default for ComponentPreviewOptions {
fn default() -> Self {
Self {
width: None,
height: None,
dpi_factor: 1.0,
background_color: ColorU {
r: 255,
g: 255,
b: 255,
a: 255,
},
}
}
}
#[derive(Debug)]
pub struct ComponentPreviewResult {
pub png_data: Vec<u8>,
pub content_width: f32,
pub content_height: f32,
}
#[allow(clippy::match_same_arms)] fn compute_content_bounds(dl: &DisplayList) -> Option<(f32, f32, f32, f32)> {
let mut min_x = f32::MAX;
let mut min_y = f32::MAX;
let mut max_x = f32::MIN;
let mut max_y = f32::MIN;
let mut has_items = false;
for item in &dl.items {
let bounds = match item {
DisplayListItem::Rect { bounds, .. } => Some(*bounds),
DisplayListItem::SelectionRect { bounds, .. } => Some(*bounds),
DisplayListItem::Border { bounds, .. } => Some(*bounds),
DisplayListItem::Text { clip_rect, .. } => Some(*clip_rect),
DisplayListItem::Image { bounds, .. } => Some(*bounds),
DisplayListItem::BoxShadow { bounds, .. } => Some(*bounds),
DisplayListItem::PushClip { bounds, .. } => Some(*bounds),
DisplayListItem::LinearGradient { bounds, .. } => Some(*bounds),
DisplayListItem::RadialGradient { bounds, .. } => Some(*bounds),
DisplayListItem::ConicGradient { bounds, .. } => Some(*bounds),
DisplayListItem::VirtualView { bounds, .. } => Some(*bounds),
DisplayListItem::ScrollBar { bounds, .. } => Some(*bounds),
_ => None,
};
if let Some(b) = bounds {
has_items = true;
min_x = min_x.min(b.0.origin.x);
min_y = min_y.min(b.0.origin.y);
max_x = max_x.max(b.0.origin.x + b.0.size.width);
max_y = max_y.max(b.0.origin.y + b.0.size.height);
}
}
if has_items {
Some((min_x, min_y, max_x, max_y))
} else {
None
}
}
#[cfg(all(feature = "std", feature = "text_layout", feature = "font_loading"))]
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] #[allow(clippy::too_many_lines)] pub fn render_component_preview(
styled_dom: &azul_core::styled_dom::StyledDom,
font_manager: &FontManager<FontRef>,
opts: ComponentPreviewOptions,
system_style: Option<std::sync::Arc<azul_css::system::SystemStyle>>,
) -> Result<ComponentPreviewResult, String> {
use crate::{
font_traits::TextLayoutCache,
solver3::{self, cache::LayoutCache, display_list::DisplayList},
};
use azul_core::{
dom::DomId,
geom::{LogicalPosition, LogicalRect, LogicalSize},
resources::{IdNamespace, RendererResources},
selection::{SelectionState, TextSelection},
};
use std::collections::{BTreeMap, HashMap};
const MAX_SIZE: f32 = 4096.0;
let layout_width = opts.width.unwrap_or(MAX_SIZE);
let layout_height = opts.height.unwrap_or(MAX_SIZE);
let viewport = LogicalRect {
origin: LogicalPosition::zero(),
size: LogicalSize {
width: layout_width,
height: layout_height,
},
};
let mut preview_font_manager = FontManager::from_arc_shared(
font_manager.fc_cache.clone(),
font_manager.parsed_fonts.clone(),
)
.map_err(|e| format!("Failed to create preview font manager: {e:?}"))?;
{
use crate::solver3::getters::collect_and_resolve_font_chains_with_registration;
use crate::text3::default::PathLoader;
let platform = azul_css::system::Platform::current();
let chains = collect_and_resolve_font_chains_with_registration(
styled_dom,
&preview_font_manager.fc_cache,
&preview_font_manager,
&platform,
);
let loader = PathLoader::new();
let _failed = preview_font_manager.load_missing_for_chains(&chains, |bytes, index| {
loader.load_font_shared(bytes, index)
});
preview_font_manager.set_font_chain_cache(chains.into_fontconfig_chains());
}
let mut layout_cache = LayoutCache {
tree: None,
calculated_positions: Vec::new(),
viewport: None,
scroll_ids: HashMap::new(),
scroll_id_to_node_id: HashMap::new(),
counters: HashMap::new(),
float_cache: HashMap::new(),
cache_map: solver3::cache::LayoutCacheMap::default(),
previous_positions: Vec::new(),
cached_display_list: None,
prev_dom_ptr: 0,
prev_viewport: LogicalRect::zero(),
};
let mut text_cache = TextLayoutCache::new();
let empty_scroll_offsets = BTreeMap::new();
let empty_text_selections = BTreeMap::new();
let renderer_resources = RendererResources::default();
let id_namespace = IdNamespace(0xFFFF);
let dom_id = DomId::ROOT_ID;
let mut debug_messages = None;
let get_system_time_fn = azul_core::task::GetSystemTimeCallback {
cb: azul_core::task::get_system_time_libstd,
};
let display_list = solver3::layout_document(
&mut layout_cache,
&mut text_cache,
styled_dom,
viewport,
&preview_font_manager,
&empty_scroll_offsets,
&empty_text_selections,
&mut debug_messages,
None,
&renderer_resources,
id_namespace,
dom_id,
false,
Vec::new(),
None, &azul_core::resources::ImageCache::default(),
system_style.clone(),
get_system_time_fn,
)
.map_err(|e| format!("Layout failed: {e:?}"))?;
let (render_width, render_height) = if opts.width.is_some() && opts.height.is_some() {
(opts.width.unwrap(), opts.height.unwrap())
} else {
match compute_content_bounds(&display_list) {
Some((_min_x, _min_y, max_x, max_y)) => {
let w = if opts.width.is_some() {
opts.width.unwrap()
} else {
max_x.max(1.0).ceil()
};
let h = if opts.height.is_some() {
opts.height.unwrap()
} else {
max_y.max(1.0).ceil()
};
(w, h)
}
None => {
return Ok(ComponentPreviewResult {
png_data: Vec::new(),
content_width: 0.0,
content_height: 0.0,
});
}
}
};
let render_width = render_width.min(MAX_SIZE);
let render_height = render_height.min(MAX_SIZE);
let dpi = opts.dpi_factor;
let pixel_w = ((render_width * dpi) as u32).max(1);
let pixel_h = ((render_height * dpi) as u32).max(1);
let mut pixmap = AzulPixmap::new(pixel_w, pixel_h)
.ok_or_else(|| format!("Cannot create pixmap {pixel_w}x{pixel_h}"))?;
let bg = opts.background_color;
pixmap.fill(bg.r, bg.g, bg.b, bg.a);
let mut preview_glyph_cache = GlyphCache::new();
let preview_render_state =
CpuRenderState::new(ScrollOffsetMap::new()).with_system_style(system_style);
render_display_list_with_state(
&display_list,
&mut pixmap,
dpi,
&renderer_resources,
Some(&preview_font_manager),
&mut preview_glyph_cache,
&preview_render_state,
)?;
let png_data = pixmap
.encode_png()
.map_err(|e| format!("PNG encoding failed: {e}"))?;
Ok(ComponentPreviewResult {
png_data,
content_width: render_width,
content_height: render_height,
})
}
#[cfg(all(feature = "std", feature = "text_layout", feature = "font_loading"))]
pub fn render_dom_to_image(
mut dom: azul_core::dom::Dom,
css: azul_css::css::Css,
width: f32,
height: f32,
dpi: f32,
) -> Result<Vec<u8>, String> {
use crate::font_traits::FontManager;
use azul_core::styled_dom::StyledDom;
let styled_dom = StyledDom::create(&mut dom, css);
let fc_cache = crate::font::loading::build_font_cache();
let font_manager = FontManager::new(fc_cache)
.map_err(|e| format!("Failed to create font manager: {e:?}"))?;
let opts = ComponentPreviewOptions {
width: Some(width),
height: Some(height),
dpi_factor: dpi,
background_color: ColorU {
r: 255,
g: 255,
b: 255,
a: 255,
},
};
let result = render_component_preview(&styled_dom, &font_manager, opts, None)?;
Ok(result.png_data)
}
#[cfg(all(feature = "std", feature = "text_layout", feature = "font_loading"))]
#[must_use]
#[allow(clippy::suboptimal_flops, clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub fn render_text_run_to_pixmap(
fc_cache: &rust_fontconfig::FcFontCache,
text: &str,
font_size_px: f32,
text_color: ColorU,
bg_color: ColorU,
padding_px: f32,
dpi_factor: f32,
) -> Option<AzulPixmap> {
use azul_core::resources::{FontKey, IdNamespace};
use rust_fontconfig::{FcPattern, OwnedFontSource};
let mut trace = Vec::new();
let matched = fc_cache
.query(
&FcPattern {
family: Some("sans-serif".to_string()),
..Default::default()
},
&mut trace,
)
.or_else(|| fc_cache.query(&FcPattern::default(), &mut trace))?;
let bytes = fc_cache.get_font_bytes(&matched.id)?;
let font_index = fc_cache
.get_font_by_id(&matched.id)
.map_or(0, |src| match src {
OwnedFontSource::Disk(path) => path.font_index,
OwnedFontSource::Memory(font) => font.font_index,
});
let parsed = ParsedFont::from_bytes(bytes.as_slice(), font_index, &mut Vec::new())?
.with_source_bytes(bytes.clone());
let upm = f32::from(parsed.font_metrics.units_per_em);
if upm <= 0.0 {
return None;
}
let scale = font_size_px / upm;
let mut rr = RendererResources::default();
let font_ref = crate::parsed_font_to_font_ref(parsed.clone());
let key = FontKey::unique(IdNamespace(0));
let hash = crate::font_ref_to_parsed_font(&font_ref).hash;
rr.font_hash_map.insert(hash, key);
rr.currently_registered_fonts
.insert(key, (font_ref, std::collections::BTreeMap::default()));
let font_hash = FontHash { font_hash: hash };
let ascent = parsed.font_metrics.ascent * scale;
let descent = parsed.font_metrics.descent * scale; let baseline_y = padding_px + ascent;
let mut pen_x = padding_px;
let mut glyphs = Vec::new();
for c in text.chars() {
let gid = parsed.lookup_glyph_index(c as u32).unwrap_or(0);
let advance = f32::from(parsed.get_horizontal_advance(gid)) * scale;
glyphs.push(GlyphInstance {
index: u32::from(gid),
point: LogicalPosition { x: pen_x, y: baseline_y },
size: LogicalSize { width: advance, height: font_size_px },
});
pen_x += advance;
}
let logical_w = (pen_x + padding_px).max(1.0);
let logical_h = (ascent - descent + padding_px * 2.0).max(1.0);
let w = ((logical_w * dpi_factor).ceil() as u32).max(1);
let h = ((logical_h * dpi_factor).ceil() as u32).max(1);
let mut pixmap = AzulPixmap::new(w, h)?;
pixmap.fill(bg_color.r, bg_color.g, bg_color.b, bg_color.a);
let clip_rect: crate::solver3::display_list::WindowLogicalRect = LogicalRect {
origin: LogicalPosition { x: 0.0, y: 0.0 },
size: LogicalSize { width: logical_w, height: logical_h },
}
.into();
let item = DisplayListItem::Text {
glyphs,
font_hash,
font_size_px,
color: text_color,
clip_rect,
source_node_index: None,
};
let dl = DisplayList {
items: vec![item],
..Default::default()
};
let mut gc = GlyphCache::new();
render_display_list(&dl, &mut pixmap, dpi_factor, &rr, None, &mut gc).ok()?;
Some(pixmap)
}
#[cfg(all(test, feature = "std"))]
mod text_shadow_tests {
use super::*;
use crate::font::parsed::ParsedFont;
use crate::solver3::display_list::{DisplayList, WindowLogicalRect};
use azul_core::resources::{FontKey, IdNamespace};
use azul_css::props::basic::pixel::{PixelValue, PixelValueNoPercent};
use azul_css::props::style::box_shadow::StyleBoxShadow;
fn load_test_font() -> Option<ParsedFont> {
let candidates = [
"/System/Library/Fonts/Supplemental/Times New Roman.ttf",
"/System/Library/Fonts/Helvetica.ttc",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
"C:/Windows/Fonts/arial.ttf",
];
for path in candidates {
if let Ok(bytes) = std::fs::read(path) {
let arc = std::sync::Arc::new(rust_fontconfig::FontBytes::Owned(
std::sync::Arc::from(bytes.as_slice()),
));
if let Some(font) = ParsedFont::from_bytes(&bytes, 0, &mut Vec::new())
.map(|f| f.with_source_bytes(arc))
{
return Some(font);
}
}
}
None
}
fn renderer_resources_with(font: &ParsedFont) -> (RendererResources, FontHash) {
let mut rr = RendererResources::default();
let font_ref = crate::parsed_font_to_font_ref(font.clone());
let key = FontKey::unique(IdNamespace(0));
let hash = crate::font_ref_to_parsed_font(&font_ref).hash;
rr.font_hash_map.insert(hash, key);
rr.currently_registered_fonts
.insert(key, (font_ref, std::collections::BTreeMap::default()));
(rr, FontHash { font_hash: hash })
}
fn shape(parsed: &ParsedFont, text: &str, font_size: f32, x: f32, y: f32) -> Vec<GlyphInstance> {
let upm = f32::from(parsed.font_metrics.units_per_em);
let scale = font_size / upm;
let mut pen_x = x;
let mut out = Vec::new();
for c in text.chars() {
let gid = parsed.lookup_glyph_index(c as u32).unwrap_or(0);
let advance = f32::from(parsed.get_horizontal_advance(gid)) * scale;
out.push(GlyphInstance {
index: u32::from(gid),
point: LogicalPosition { x: pen_x, y },
size: LogicalSize {
width: advance,
height: font_size,
},
});
pen_x += advance;
}
out
}
fn count_red(pixmap: &AzulPixmap) -> usize {
pixmap
.data()
.chunks_exact(4)
.filter(|p| p[0] > 150 && p[1] < 100 && p[2] < 100)
.count()
}
#[test]
fn text_shadow_paints_offset_colored_pixels() {
let Some(font) = load_test_font() else {
eprintln!("[skip] no system font available");
return;
};
let (rr, font_hash) = renderer_resources_with(&font);
let w = 200u32;
let h = 60u32;
let font_size = 32.0;
let glyphs = shape(&font, "Hi", font_size, 10.0, 40.0);
#[allow(clippy::cast_precision_loss)]
let clip_rect: WindowLogicalRect = LogicalRect {
origin: LogicalPosition { x: 0.0, y: 0.0 },
size: LogicalSize { width: w as f32, height: h as f32 },
}
.into();
let text_item = DisplayListItem::Text {
glyphs,
font_hash,
font_size_px: font_size,
color: ColorU { r: 0, g: 0, b: 0, a: 255 },
clip_rect,
source_node_index: None,
};
let mut gc = GlyphCache::new();
let mut no_shadow = AzulPixmap::new(w, h).unwrap();
no_shadow.fill(255, 255, 255, 255);
let dl_plain = DisplayList {
items: vec![text_item.clone()],
..Default::default()
};
render_display_list(&dl_plain, &mut no_shadow, 1.0, &rr, None, &mut gc).unwrap();
let red_plain = count_red(&no_shadow);
let shadow = StyleBoxShadow {
offset_x: PixelValueNoPercent { inner: PixelValue::px(24.0) },
offset_y: PixelValueNoPercent { inner: PixelValue::px(0.0) },
blur_radius: PixelValueNoPercent { inner: PixelValue::px(0.0) },
spread_radius: PixelValueNoPercent { inner: PixelValue::px(0.0) },
color: ColorU { r: 255, g: 0, b: 0, a: 255 },
clip_mode: azul_css::props::style::box_shadow::BoxShadowClipMode::Outset,
};
let mut with_shadow = AzulPixmap::new(w, h).unwrap();
with_shadow.fill(255, 255, 255, 255);
let dl_shadow = DisplayList {
items: vec![
DisplayListItem::PushTextShadow { shadow },
text_item,
DisplayListItem::PopTextShadow,
],
..Default::default()
};
let mut gc2 = GlyphCache::new();
render_display_list(&dl_shadow, &mut with_shadow, 1.0, &rr, None, &mut gc2).unwrap();
let red_shadow = count_red(&with_shadow);
assert!(
red_shadow > red_plain + 20,
"text-shadow must paint red shadow pixels beyond the baseline \
(plain {red_plain}, shadow {red_shadow})"
);
let right_red = with_shadow
.data()
.chunks_exact(4)
.enumerate()
.filter(|(i, p)| {
#[allow(clippy::cast_possible_truncation)] let x = (*i as u32) % w;
x > 30 && p[0] > 150 && p[1] < 100 && p[2] < 100
})
.count();
assert!(
right_red > 0,
"shadow should appear offset to the right of the glyphs"
);
}
#[test]
fn text_shadow_blur_spreads_coverage() {
let Some(font) = load_test_font() else {
eprintln!("[skip] no system font available");
return;
};
let (rr, font_hash) = renderer_resources_with(&font);
let w = 200u32;
let h = 80u32;
let font_size = 32.0;
let glyphs = shape(&font, "Hi", font_size, 40.0, 50.0);
#[allow(clippy::cast_precision_loss)]
let clip_rect: WindowLogicalRect = LogicalRect {
origin: LogicalPosition { x: 0.0, y: 0.0 },
size: LogicalSize { width: w as f32, height: h as f32 },
}
.into();
let make = |blur: f32| -> usize {
let shadow = StyleBoxShadow {
offset_x: PixelValueNoPercent { inner: PixelValue::px(0.0) },
offset_y: PixelValueNoPercent { inner: PixelValue::px(0.0) },
blur_radius: PixelValueNoPercent { inner: PixelValue::px(blur) },
spread_radius: PixelValueNoPercent { inner: PixelValue::px(0.0) },
color: ColorU { r: 255, g: 0, b: 0, a: 255 },
clip_mode: azul_css::props::style::box_shadow::BoxShadowClipMode::Outset,
};
let text_item = DisplayListItem::Text {
glyphs: glyphs.clone(),
font_hash,
font_size_px: font_size,
color: ColorU { r: 0, g: 0, b: 0, a: 0 }, clip_rect,
source_node_index: None,
};
let dl = DisplayList {
items: vec![
DisplayListItem::PushTextShadow { shadow },
text_item,
DisplayListItem::PopTextShadow,
],
..Default::default()
};
let mut pm = AzulPixmap::new(w, h).unwrap();
pm.fill(255, 255, 255, 255);
let mut gc = GlyphCache::new();
render_display_list(&dl, &mut pm, 1.0, &rr, None, &mut gc).unwrap();
pm.data()
.chunks_exact(4)
.filter(|p| p[0] != 255 || p[1] != 255 || p[2] != 255)
.count()
};
let hard = make(0.0);
let blurred = make(6.0);
assert!(hard > 0, "hard shadow should paint");
assert!(
blurred > hard,
"blurred shadow ({blurred}) should cover more pixels than hard ({hard})"
);
}
}