mod background;
mod box_shadow;
mod form_controls;
use std::any::Any;
use super::kurbo_css::{CssBox, Edge};
use crate::color::{Color, ToColorColor};
use crate::debug_overlay::render_debug_overlay;
use crate::kurbo_css::NonUniformRoundedRectRadii;
use crate::layers::maybe_with_layer;
use crate::sizing::compute_object_fit;
use anyrender::{CustomPaint, Paint, PaintScene};
use blitz_dom::node::{
ListItemLayout, ListItemLayoutPosition, Marker, NodeData, RasterImageData, TextInputData,
TextNodeData,
};
use blitz_dom::{BaseDocument, ElementData, Node, local_name};
use blitz_traits::devtools::DevtoolSettings;
use euclid::Transform3D;
use style::values::computed::BorderCornerRadius;
use style::{
dom::TElement,
properties::{
ComputedValues, generated::longhands::visibility::computed_value::T as StyloVisibility,
style_structs::Font,
},
values::{
computed::{CSSPixelLength, Overflow},
specified::{BorderStyle, OutlineStyle, image::ImageRendering},
},
};
use kurbo::{self, Affine, Insets, Point, Rect, Stroke, Vec2};
use peniko::{self, Fill, ImageData, ImageSampler};
use style::values::generics::color::GenericColor;
use taffy::Layout;
pub struct BlitzDomPainter<'dom> {
pub(crate) dom: &'dom BaseDocument,
pub(crate) scale: f64,
pub(crate) width: u32,
pub(crate) height: u32,
pub(crate) devtools: DevtoolSettings,
}
impl BlitzDomPainter<'_> {
fn node_position(&self, node: usize, location: Point) -> (Layout, Point) {
let layout = self.layout(node);
let pos = location + Vec2::new(layout.location.x as f64, layout.location.y as f64);
(layout, pos)
}
fn layout(&self, child: usize) -> Layout {
self.dom.as_ref().tree()[child].unrounded_layout
}
pub fn paint_scene(&self, scene: &mut impl PaintScene) {
scene.reset();
let viewport_scroll = self.dom.as_ref().viewport_scroll();
let root_element = self.dom.as_ref().root_element();
let root_id = root_element.id;
let bg_width = (self.width as f32).max(root_element.final_layout.size.width);
let bg_height = (self.height as f32).max(root_element.final_layout.size.height);
let background_color = {
let html_color = root_element
.primary_styles()
.map(|s| s.clone_background_color())
.unwrap_or(GenericColor::TRANSPARENT_BLACK);
if html_color == GenericColor::TRANSPARENT_BLACK {
root_element
.children
.iter()
.find_map(|id| {
self.dom
.as_ref()
.get_node(*id)
.filter(|node| node.data.is_element_with_tag_name(&local_name!("body")))
})
.and_then(|body| body.primary_styles())
.map(|style| {
let current_color = style.clone_color();
style
.clone_background_color()
.resolve_to_absolute(¤t_color)
})
} else {
let current_color = root_element.primary_styles().unwrap().clone_color();
Some(html_color.resolve_to_absolute(¤t_color))
}
};
if let Some(bg_color) = background_color {
let bg_color = bg_color.as_srgb_color();
let rect = Rect::from_origin_size((0.0, 0.0), (bg_width as f64, bg_height as f64));
scene.fill(Fill::NonZero, Affine::IDENTITY, bg_color, None, &rect);
}
self.render_element(
scene,
root_id,
Point {
x: -viewport_scroll.x,
y: -viewport_scroll.y,
},
);
if self.devtools.highlight_hover {
if let Some(node_id) = self.dom.as_ref().get_hover_node_id() {
render_debug_overlay(scene, self.dom, node_id, self.scale);
}
}
}
fn render_element(&self, scene: &mut impl PaintScene, node_id: usize, location: Point) {
let node = &self.dom.as_ref().tree()[node_id];
if matches!(node.style.display, taffy::Display::None) {
return;
}
if node.primary_styles().is_none() {
return;
}
if node.local_name() == "input" && node.attr(local_name!("type")) == Some("hidden") {
return;
}
if node
.primary_styles()
.unwrap()
.get_inherited_box()
.visibility
!= StyloVisibility::Visible
{
return;
}
let opacity = node.primary_styles().unwrap().get_effects().opacity;
if opacity == 0.0 {
return;
}
let has_opacity = opacity < 1.0;
let styles = &node.primary_styles().unwrap();
let overflow_x = styles.get_box().overflow_x;
let overflow_y = styles.get_box().overflow_y;
let is_image = node
.element_data()
.and_then(|e| e.raster_image_data())
.is_some();
let should_clip = is_image
|| !matches!(overflow_x, Overflow::Visible)
|| !matches!(overflow_y, Overflow::Visible);
let (layout, box_position) = self.node_position(node_id, location);
let taffy::Layout {
size,
border,
padding,
content_size,
..
} = node.final_layout;
let scaled_pb = (padding + border).map(f64::from);
let content_position = kurbo::Point {
x: box_position.x + scaled_pb.left,
y: box_position.y + scaled_pb.top,
};
let content_box_size = kurbo::Size {
width: (size.width as f64 - scaled_pb.left - scaled_pb.right) * self.scale,
height: (size.height as f64 - scaled_pb.top - scaled_pb.bottom) * self.scale,
};
let scaled_y = box_position.y * self.scale;
let scaled_content_height = content_size.height.max(size.height) as f64 * self.scale;
if scaled_y > self.height as f64 || scaled_y + scaled_content_height < 0.0 {
return;
}
let clip_area = content_box_size.width * content_box_size.height;
if should_clip && clip_area < 0.01 {
return;
}
let mut cx = self.element_cx(node, layout, box_position);
cx.draw_outline(scene);
cx.draw_outset_box_shadow(scene);
cx.draw_background(scene);
cx.draw_border(scene);
let wants_layer = should_clip | has_opacity;
let clip = &cx.frame.padding_box_path();
maybe_with_layer(scene, wants_layer, opacity, cx.transform, clip, |scene| {
cx.draw_inset_box_shadow(scene);
cx.stroke_devtools(scene);
let content_position = Point {
x: content_position.x - node.scroll_offset.x,
y: content_position.y - node.scroll_offset.y,
};
cx.pos = Point {
x: cx.pos.x - node.scroll_offset.x,
y: cx.pos.y - node.scroll_offset.y,
};
cx.transform = cx.transform.then_translate(Vec2 {
x: -node.scroll_offset.x,
y: -node.scroll_offset.y,
});
cx.draw_image(scene);
#[cfg(feature = "svg")]
cx.draw_svg(scene);
cx.draw_canvas(scene);
cx.draw_input(scene);
cx.draw_text_input_text(scene, content_position);
cx.draw_inline_layout(scene, content_position);
cx.draw_marker(scene, content_position);
cx.draw_children(scene);
});
}
fn render_node(&self, scene: &mut impl PaintScene, node_id: usize, location: Point) {
let node = &self.dom.as_ref().tree()[node_id];
match &node.data {
NodeData::Element(_) | NodeData::AnonymousBlock(_) => {
self.render_element(scene, node_id, location)
}
NodeData::Text(TextNodeData { .. }) => {
}
NodeData::Document => {}
NodeData::Comment => {} }
}
fn element_cx<'w>(
&'w self,
node: &'w Node,
layout: Layout,
box_position: Point,
) -> ElementCx<'w> {
let style = node
.stylo_element_data
.borrow()
.as_ref()
.map(|element_data| element_data.styles.primary().clone())
.unwrap_or(
ComputedValues::initial_values_with_font_override(Font::initial_values()).to_arc(),
);
let scale = self.scale;
let frame = create_css_rect(&style, &layout, scale);
let mut transform = Affine::translate(box_position.to_vec2() * scale);
let (t, has_3d) = &style
.get_box()
.transform
.to_transform_3d_matrix(None)
.unwrap_or((Transform3D::default(), false));
if !has_3d {
let kurbo_transform =
Affine::new([t.m11, t.m12, t.m21, t.m22, t.m41, t.m42].map(|v| v as f64));
let transform_origin = &style.get_box().transform_origin;
let origin_translation = Affine::translate(Vec2 {
x: transform_origin
.horizontal
.resolve(CSSPixelLength::new(frame.border_box.width() as f32))
.px() as f64,
y: transform_origin
.vertical
.resolve(CSSPixelLength::new(frame.border_box.width() as f32))
.px() as f64,
});
let kurbo_transform =
origin_translation * kurbo_transform * origin_translation.inverse();
transform *= kurbo_transform;
}
let element = node.element_data().unwrap();
ElementCx {
context: self,
frame,
scale,
style,
pos: box_position,
node,
element,
transform,
#[cfg(feature = "svg")]
svg: element.svg_data(),
text_input: element.text_input_data(),
list_item: element.list_item_data.as_deref(),
devtools: &self.devtools,
}
}
}
fn to_image_quality(image_rendering: ImageRendering) -> peniko::ImageQuality {
match image_rendering {
ImageRendering::Auto => peniko::ImageQuality::Medium,
ImageRendering::CrispEdges => peniko::ImageQuality::Low,
ImageRendering::Pixelated => peniko::ImageQuality::Low,
}
}
fn to_peniko_image(image: &RasterImageData, quality: peniko::ImageQuality) -> peniko::ImageBrush {
peniko::ImageBrush {
image: ImageData {
data: peniko::Blob::new(image.data.clone()),
format: peniko::ImageFormat::Rgba8,
width: image.width,
height: image.height,
alpha_type: peniko::ImageAlphaType::Alpha,
},
sampler: ImageSampler {
x_extend: peniko::Extend::Repeat,
y_extend: peniko::Extend::Repeat,
quality,
alpha: 1.0,
},
}
}
struct ElementCx<'a> {
context: &'a BlitzDomPainter<'a>,
frame: CssBox,
style: style::servo_arc::Arc<ComputedValues>,
pos: Point,
scale: f64,
node: &'a Node,
element: &'a ElementData,
transform: Affine,
#[cfg(feature = "svg")]
svg: Option<&'a usvg::Tree>,
text_input: Option<&'a TextInputData>,
list_item: Option<&'a ListItemLayout>,
devtools: &'a DevtoolSettings,
}
fn convert_rect(rect: &parley::BoundingBox) -> kurbo::Rect {
peniko::kurbo::Rect::new(rect.x0, rect.y0, rect.x1, rect.y1)
}
impl ElementCx<'_> {
fn draw_inline_layout(&self, scene: &mut impl PaintScene, pos: Point) {
if self.node.flags.is_inline_root() {
let text_layout = self.element
.inline_layout_data
.as_ref()
.unwrap_or_else(|| {
panic!("Tried to render node marked as inline root that does not have an inline layout: {:?}", self.node);
});
crate::text::stroke_text(
self.scale,
scene,
text_layout.layout.lines(),
self.context.dom,
pos,
);
}
}
fn draw_text_input_text(&self, scene: &mut impl PaintScene, pos: Point) {
if let Some(input_data) = self.text_input {
let transform = Affine::translate((pos.x * self.scale, pos.y * self.scale));
if self.node.is_focussed() {
for (rect, _line_idx) in input_data.editor.selection_geometry().iter() {
scene.fill(
Fill::NonZero,
transform,
color::palette::css::STEEL_BLUE,
None,
&convert_rect(rect),
);
}
if let Some(cursor) = input_data.editor.cursor_geometry(1.5) {
scene.fill(
Fill::NonZero,
transform,
Color::BLACK,
None,
&convert_rect(&cursor),
);
};
}
crate::text::stroke_text(
self.scale,
scene,
input_data.editor.try_layout().unwrap().lines(),
self.context.dom,
pos,
);
}
}
fn draw_marker(&self, scene: &mut impl PaintScene, pos: Point) {
if let Some(ListItemLayout {
marker,
position: ListItemLayoutPosition::Outside(layout),
}) = self.list_item
{
let x_padding = match marker {
Marker::Char(_) => 8.0,
Marker::String(_) => 0.0,
};
let x_offset = -(layout.full_width() / layout.scale() + x_padding);
let y_offset = if let Some(first_text_line) = &self
.element
.inline_layout_data
.as_ref()
.and_then(|text_layout| text_layout.layout.lines().next())
{
(first_text_line.metrics().baseline
- layout.lines().next().unwrap().metrics().baseline)
/ layout.scale()
} else {
0.0
};
let pos = Point {
x: pos.x + x_offset as f64,
y: pos.y + y_offset as f64,
};
crate::text::stroke_text(self.scale, scene, layout.lines(), self.context.dom, pos);
}
}
fn draw_children(&self, scene: &mut impl PaintScene) {
if let Some(children) = &*self.node.paint_children.borrow() {
for child_id in children {
self.render_node(scene, *child_id, self.pos);
}
}
}
#[cfg(feature = "svg")]
fn draw_svg(&self, scene: &mut impl PaintScene) {
use style::properties::generated::longhands::object_fit::computed_value::T as ObjectFit;
let Some(svg) = self.svg else {
return;
};
let width = self.frame.content_box.width() as u32;
let height = self.frame.content_box.height() as u32;
let svg_size = svg.size();
let x = self.frame.content_box.origin().x;
let y = self.frame.content_box.origin().y;
let object_position = self.style.clone_object_position();
let container_size = taffy::Size {
width: width as f32,
height: height as f32,
};
let object_size = taffy::Size {
width: svg_size.width(),
height: svg_size.height(),
};
let paint_size = compute_object_fit(container_size, Some(object_size), ObjectFit::Contain);
let x_offset = object_position.horizontal.resolve(
CSSPixelLength::new(container_size.width - paint_size.width) / self.scale as f32,
) * self.scale as f32;
let y_offset = object_position.vertical.resolve(
CSSPixelLength::new(container_size.height - paint_size.height) / self.scale as f32,
) * self.scale as f32;
let x = x + x_offset.px() as f64;
let y = y + y_offset.px() as f64;
let x_scale = paint_size.width as f64 / object_size.width as f64;
let y_scale = paint_size.height as f64 / object_size.height as f64;
let transform =
Affine::translate((self.pos.x * self.scale + x, self.pos.y * self.scale + y))
.pre_scale_non_uniform(x_scale, y_scale);
anyrender_svg::render_svg_tree(scene, svg, transform);
}
fn draw_image(&self, scene: &mut impl PaintScene) {
if let Some(image) = self.element.raster_image_data() {
let width = self.frame.content_box.width() as u32;
let height = self.frame.content_box.height() as u32;
let x = self.frame.content_box.origin().x;
let y = self.frame.content_box.origin().y;
let object_fit = self.style.clone_object_fit();
let object_position = self.style.clone_object_position();
let image_rendering = self.style.clone_image_rendering();
let quality = to_image_quality(image_rendering);
let container_size = taffy::Size {
width: width as f32,
height: height as f32,
};
let object_size = taffy::Size {
width: image.width as f32,
height: image.height as f32,
};
let paint_size = compute_object_fit(container_size, Some(object_size), object_fit);
let x_offset = object_position.horizontal.resolve(
CSSPixelLength::new(container_size.width - paint_size.width) / self.scale as f32,
) * self.scale as f32;
let y_offset = object_position.vertical.resolve(
CSSPixelLength::new(container_size.height - paint_size.height) / self.scale as f32,
) * self.scale as f32;
let x = x + x_offset.px() as f64;
let y = y + y_offset.px() as f64;
let x_scale = paint_size.width as f64 / object_size.width as f64;
let y_scale = paint_size.height as f64 / object_size.height as f64;
let transform = self
.transform
.pre_scale_non_uniform(x_scale, y_scale)
.then_translate(Vec2 { x, y });
scene.draw_image(to_peniko_image(image, quality).as_ref(), transform);
}
}
fn draw_canvas(&self, scene: &mut impl PaintScene) {
if let Some(custom_paint_source) = self.element.canvas_data() {
let width = self.frame.content_box.width() as u32;
let height = self.frame.content_box.height() as u32;
let x = self.frame.content_box.origin().x;
let y = self.frame.content_box.origin().y;
let transform = self.transform.then_translate(Vec2 { x, y });
scene.fill(
Fill::NonZero,
transform,
Paint::Custom(&CustomPaint {
source_id: custom_paint_source.custom_paint_source_id,
width,
height,
scale: self.scale,
} as &(dyn Any + Send + Sync)),
None,
&Rect::from_origin_size((0.0, 0.0), (width as f64, height as f64)),
);
}
}
fn stroke_devtools(&self, scene: &mut impl PaintScene) {
if self.devtools.show_layout {
let shape = &self.frame.border_box;
let stroke = Stroke::new(self.scale);
let stroke_color = match self.node.style.display {
taffy::Display::Block => Color::new([1.0, 0.0, 0.0, 1.0]),
taffy::Display::Flex => Color::new([0.0, 1.0, 0.0, 1.0]),
taffy::Display::Grid => Color::new([0.0, 0.0, 1.0, 1.0]),
taffy::Display::None => Color::new([0.0, 0.0, 1.0, 1.0]),
};
scene.stroke(&stroke, self.transform, stroke_color, None, &shape);
}
}
fn draw_border(&self, sb: &mut impl PaintScene) {
for edge in [Edge::Top, Edge::Right, Edge::Bottom, Edge::Left] {
self.draw_border_edge(sb, edge);
}
}
fn draw_border_edge(&self, sb: &mut impl PaintScene, edge: Edge) {
let style = &*self.style;
let border = style.get_border();
let path = self.frame.border_edge_shape(edge);
let current_color = style.clone_color();
let color = match edge {
Edge::Top => border
.border_top_color
.resolve_to_absolute(¤t_color)
.as_srgb_color(),
Edge::Right => border
.border_right_color
.resolve_to_absolute(¤t_color)
.as_srgb_color(),
Edge::Bottom => border
.border_bottom_color
.resolve_to_absolute(¤t_color)
.as_srgb_color(),
Edge::Left => border
.border_left_color
.resolve_to_absolute(¤t_color)
.as_srgb_color(),
};
let alpha = color.components[3];
if alpha != 0.0 {
sb.fill(Fill::NonZero, self.transform, color, None, &path);
}
}
fn draw_outline(&self, scene: &mut impl PaintScene) {
let outline = self.style.get_outline();
let current_color = self.style.clone_color();
let color = outline
.outline_color
.resolve_to_absolute(¤t_color)
.as_srgb_color();
let style = match outline.outline_style {
OutlineStyle::Auto => return,
OutlineStyle::BorderStyle(style) => style,
};
let path = match style {
BorderStyle::None | BorderStyle::Hidden => return,
BorderStyle::Solid => self.frame.outline(),
BorderStyle::Inset
| BorderStyle::Groove
| BorderStyle::Outset
| BorderStyle::Ridge
| BorderStyle::Dotted
| BorderStyle::Dashed
| BorderStyle::Double => self.frame.outline(),
};
scene.fill(Fill::NonZero, self.transform, color, None, &path);
}
}
impl<'a> std::ops::Deref for ElementCx<'a> {
type Target = BlitzDomPainter<'a>;
fn deref(&self) -> &Self::Target {
self.context
}
}
fn insets_from_taffy_rect(input: taffy::Rect<f64>) -> Insets {
Insets {
x0: input.left,
y0: input.top,
x1: input.right,
y1: input.bottom,
}
}
fn create_css_rect(style: &ComputedValues, layout: &Layout, scale: f64) -> CssBox {
let width: f64 = layout.size.width as f64;
let height: f64 = layout.size.height as f64;
let border_box = Rect::new(0.0, 0.0, width * scale, height * scale);
let border = insets_from_taffy_rect(layout.border.map(|p| p as f64 * scale));
let padding = insets_from_taffy_rect(layout.padding.map(|p| p as f64 * scale));
let outline_width = style.get_outline().outline_width.to_f64_px() * scale;
let resolve_w = CSSPixelLength::new(width as _);
let resolve_h = CSSPixelLength::new(height as _);
let resolve_radii = |radius: &BorderCornerRadius| -> Vec2 {
Vec2 {
x: scale * radius.0.width.0.resolve(resolve_w).px() as f64,
y: scale * radius.0.height.0.resolve(resolve_h).px() as f64,
}
};
let s_border = style.get_border();
let border_radii = NonUniformRoundedRectRadii {
top_left: resolve_radii(&s_border.border_top_left_radius),
top_right: resolve_radii(&s_border.border_top_right_radius),
bottom_right: resolve_radii(&s_border.border_bottom_right_radius),
bottom_left: resolve_radii(&s_border.border_bottom_left_radius),
};
CssBox::new(border_box, border, padding, outline_width, border_radii)
}