mod background;
mod border;
mod box_shadow;
mod clip_path;
mod form_controls;
mod mask;
use std::collections::HashMap;
use std::sync::Arc;
use super::kurbo_css::CssBox;
use crate::color::{Color, ToColorColor};
use crate::debug_overlay::render_debug_overlay;
use crate::filters::convert_filters;
use crate::kurbo_css::NonUniformRoundedRectRadii;
use crate::layers::LayerManager;
use crate::sizing::compute_object_fit;
use crate::{CustomWidgetSceneMap, SELECTION_COLOR};
use anyrender::{PaintScene, Scene};
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 style::values::computed::{BorderCornerRadius, ColorOrAuto};
use style::{
dom::TElement,
properties::{
ComputedValues, generated::longhands::visibility::computed_value::T as StyloVisibility,
style_structs::Font,
},
values::{
computed::{CSSPixelLength, Overflow},
specified::image::ImageRendering,
},
};
use kurbo::{self, Affine, Insets, Point, Rect, Shape, Size, Stroke, Vec2};
use peniko::{self, Fill, ImageData, ImageSampler};
use style::values::generics::color::GenericColor;
use taffy::Layout;
pub struct BlitzDomPainter<'dom, 'a> {
pub(crate) dom: &'dom BaseDocument,
pub(crate) scale: f64,
pub(crate) width: u32,
pub(crate) height: u32,
pub(crate) initial_x: f64,
pub(crate) initial_y: f64,
pub(crate) root_element_id: Option<usize>,
#[cfg(feature = "scrollbars")]
pub(crate) hovered_scrollbar: Option<blitz_dom::node::ScrollbarRef>,
#[cfg(feature = "scrollbars")]
pub(crate) scrollbar_drag_target: Option<blitz_dom::node::ScrollbarRef>,
pub(crate) layer_manager: LayerManager,
pub(crate) selection_ranges: HashMap<usize, (usize, usize)>,
pub(crate) custom_widget_scenes: &'a CustomWidgetSceneMap,
}
impl<'dom, 'a> BlitzDomPainter<'dom, 'a> {
pub fn new(
dom: &'dom BaseDocument,
scale: f64,
width: u32,
height: u32,
initial_x: f64,
initial_y: f64,
custom_widget_scenes: &'a CustomWidgetSceneMap,
) -> Self {
let selection_ranges: HashMap<usize, (usize, usize)> = dom
.get_text_selection_ranges()
.into_iter()
.map(|(node_id, start, end)| (node_id, (start, end)))
.collect();
let layer_manager = LayerManager::default();
let root_element_id = dom.try_root_element().map(|el| el.id);
Self {
dom,
scale,
width,
height,
initial_x,
initial_y,
root_element_id,
#[cfg(feature = "scrollbars")]
hovered_scrollbar: dom.hovered_scrollbar(),
#[cfg(feature = "scrollbars")]
scrollbar_drag_target: dom.scrollbar_drag_target(),
layer_manager,
selection_ranges,
custom_widget_scenes,
}
}
pub fn paint_scene(&self, scene: &mut impl PaintScene) {
if self.dom.has_pending_critical_resources() {
return;
}
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(
(self.initial_x * self.scale, self.initial_y * self.scale),
(bg_width as f64, bg_height as f64),
);
scene.fill(Fill::NonZero, Affine::IDENTITY, bg_color, None, &rect);
}
let viewport_clip_rect = Rect::new(0.0, 0.0, self.width as f64, self.height as f64);
self.render_element(
scene,
root_id,
Affine::translate(Vec2 {
x: self.initial_x - (viewport_scroll.x * self.scale),
y: self.initial_y - (viewport_scroll.y * self.scale),
}),
viewport_clip_rect,
);
if self.dom.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,
self.initial_x,
self.initial_y,
);
}
}
}
fn render_element(
&self,
scene: &mut impl PaintScene,
node_id: usize,
parent_style_transform: Affine,
clip_rect: Rect,
) {
let node = &self.dom.as_ref().tree()[node_id];
if matches!(node.style.display, taffy::Display::None) {
return;
}
let Some(styles) = node.primary_styles() else {
return;
};
if node.local_name() == "input" && node.attr(local_name!("type")) == Some("hidden") {
return;
}
if styles.get_inherited_box().visibility != StyloVisibility::Visible {
return;
}
let effects = styles.get_effects();
let opacity = effects.opacity;
if opacity == 0.0 {
return;
}
let has_opacity = opacity < 1.0;
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 is_sub_doc = node
.element_data()
.and_then(|el| el.sub_doc_data())
.is_some();
let is_text_input = node
.element_data()
.and_then(|el| el.text_input_data())
.is_some();
let is_root_element = self.root_element_id == Some(node_id);
let should_clip = !is_root_element
&& (is_image
|| is_sub_doc
|| is_text_input
|| !matches!(overflow_x, Overflow::Visible)
|| !matches!(overflow_y, Overflow::Visible));
let taffy::Layout {
size,
border,
padding,
location,
..
} = node.final_layout;
let box_position = Vec2::new(location.x as f64, location.y as f64) * self.scale;
let box_size = Size::new(size.width as f64, size.height as f64);
let border_box = Rect::from_origin_size(box_position.to_point(), box_size);
let scaled_pb = (padding + border).map(f64::from);
let content_position = kurbo::Point {
x: scaled_pb.left,
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 overflow = node.scrollable_overflow;
let transform = parent_style_transform
* Affine::translate(box_position)
* node.transform.unwrap_or_default();
let screen_transform = Affine::translate(Vec2 {
x: -self.initial_x,
y: -self.initial_y,
}) * transform;
let screen_bbox = screen_transform.transform_rect_bbox(overflow.union(border_box));
if screen_bbox.x1 < clip_rect.x0
|| screen_bbox.x0 > clip_rect.x1
|| screen_bbox.y1 < clip_rect.y0
|| screen_bbox.y0 > clip_rect.y1
{
return;
}
let clip_area = content_box_size.width * content_box_size.height;
let overflow_area = node.scrollable_overflow.width() * node.scrollable_overflow.height();
if should_clip && clip_area < 0.01 && overflow_area < 0.01 {
return;
}
#[cfg(feature = "custom-widget")]
let custom_widget_scene = self.custom_widget_scenes.get(&(self.dom.id(), node_id));
#[cfg(not(feature = "custom-widget"))]
let custom_widget_scene = None;
let mut cx = self.element_cx(node, node.final_layout, transform, custom_widget_scene);
let child_clip_rect = if should_clip {
let clip_box = if is_text_input {
cx.frame.content_box_path()
} else {
cx.frame.padding_box_path()
};
clip_rect.intersect(screen_transform.transform_rect_bbox(clip_box.bounding_box()))
} else {
clip_rect
};
let clip_path_shape = cx.clip_path_shape();
let has_clip_path = clip_path_shape.is_some();
let default_clip = cx.frame.border_box_path();
let mut clip_path_for_layer = clip_path_shape.unwrap_or(default_clip);
clip_path_for_layer.apply_affine(Affine::scale(self.scale));
cx.draw_outline(scene);
cx.draw_outset_box_shadow(scene);
self.layer_manager.maybe_with_layer(
scene,
has_clip_path,
1.0,
cx.transform,
&clip_path_for_layer,
None,
None,
|scene| {
let mask_layer_pushed = cx.maybe_push_css_mask_layer(scene);
let unscrolled_transform = cx.transform;
let filter = convert_filters(&effects.filter.0).map(Arc::new);
let backdrop_filter = convert_filters(&effects.backdrop_filter.0).map(Arc::new);
let filter_expansion_area = filter
.as_ref()
.map(|f| f.expansion_rect())
.unwrap_or(Rect::ZERO);
let mut effect_layer_clip = cx.frame.border_box_path().bounding_box();
effect_layer_clip.x0 += filter_expansion_area.x0;
effect_layer_clip.y0 += filter_expansion_area.y0;
effect_layer_clip.x1 += filter_expansion_area.x1;
effect_layer_clip.y1 += filter_expansion_area.y1;
self.layer_manager.maybe_with_layer(
scene,
has_opacity || filter.is_some() || backdrop_filter.is_some(),
opacity,
cx.transform,
&effect_layer_clip,
filter,
backdrop_filter,
|scene| {
cx.draw_background(scene);
cx.draw_inset_box_shadow(scene);
cx.draw_table_row_backgrounds(scene);
cx.draw_table_borders(scene);
cx.draw_border(scene);
cx.stroke_devtools(scene);
let clip = if is_text_input {
&cx.frame.content_box_path()
} else {
&cx.frame.padding_box_path()
};
self.layer_manager.maybe_with_layer(
scene,
should_clip,
1.0, cx.transform,
clip,
None,
None,
|scene| {
let content_position = Point {
x: content_position.x - node.scroll_offset.x,
y: content_position.y - node.scroll_offset.y,
};
cx.transform = cx.transform.then_translate(Vec2 {
x: -node.scroll_offset.x * self.scale,
y: -node.scroll_offset.y * self.scale,
});
cx.draw_image(scene);
#[cfg(feature = "svg")]
cx.draw_svg(scene);
#[cfg(feature = "custom-widget")]
cx.draw_custom_widget(scene);
cx.draw_sub_document(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, cx.transform, child_clip_rect);
},
);
#[cfg(feature = "scrollbars")]
{
cx.transform = unscrolled_transform;
cx.draw_scrollbars(scene);
}
},
);
cx.transform = unscrolled_transform;
cx.maybe_pop_css_mask_layer(scene, mask_layer_pushed);
},
);
}
fn render_node(
&self,
scene: &mut impl PaintScene,
node_id: usize,
parent_style_transform: Affine,
clip_rect: Rect,
) {
let node = &self.dom.as_ref().tree()[node_id];
match &node.data {
NodeData::Element(_) | NodeData::AnonymousBlock(_) => {
self.render_element(scene, node_id, parent_style_transform, clip_rect)
}
NodeData::Text(TextNodeData { .. }) => {
}
NodeData::Document => {}
NodeData::Comment => {} }
}
fn element_cx(
&'dom self,
node: &'dom Node,
layout: Layout,
transform: Affine,
custom_widget_scene: Option<&'a Scene>,
) -> ElementCx<'dom, 'a> {
let style = node
.stylo_element_data
.primary_styles()
.as_ref()
.map(|styles| (*styles).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 element = node.element_data().unwrap();
ElementCx {
context: self,
frame,
scale,
style,
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.dom.devtools(),
custom_widget_scene,
}
}
}
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: 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<'dom, 'a> {
context: &'dom BlitzDomPainter<'dom, 'a>,
frame: CssBox,
style: style::servo_arc::Arc<ComputedValues>,
scale: f64,
node: &'dom Node,
element: &'dom ElementData,
transform: Affine,
#[cfg(feature = "svg")]
svg: Option<&'dom usvg::Tree>,
text_input: Option<&'dom TextInputData>,
list_item: Option<&'dom ListItemLayout>,
devtools: &'dom DevtoolSettings,
#[cfg_attr(not(feature = "custom-widget"), expect(unused))]
custom_widget_scene: Option<&'a Scene>,
}
fn convert_rect(rect: &parley::BoundingBox) -> kurbo::Rect {
peniko::kurbo::Rect::new(rect.x0, rect.y0, rect.x1, rect.y1)
}
impl ElementCx<'_, '_> {
#[cfg(feature = "scrollbars")]
fn draw_scrollbars(&self, scene: &mut impl PaintScene) {
use blitz_dom::node::{ScrollbarColor, ScrollbarRef};
use taffy::AbsoluteAxis;
let (custom_thumb, custom_track) = match self.node.scrollbar_color() {
ScrollbarColor::Auto => (None, None),
ScrollbarColor::Colors { thumb, track } => {
(Some(thumb.as_srgb_color()), Some(track.as_srgb_color()))
}
};
let drag_target = self.context.scrollbar_drag_target;
let hovered_thumb = self.context.hovered_scrollbar;
let node_id = self.node.id;
let opacity = self.context.dom.scrollbar_opacity(node_id);
if opacity == 0.0 {
return;
}
let dark_scheme =
self.context.dom.viewport().color_scheme == blitz_traits::shell::ColorScheme::Dark;
let (thumb_rest, thumb_hover, thumb_active, stroke_color) = if dark_scheme {
(
Color::from_rgba8(214, 214, 214, 178),
Color::from_rgba8(190, 190, 190, 222),
Color::from_rgba8(172, 172, 172, 255),
Color::from_rgba8(0, 0, 0, 102),
)
} else {
(
Color::from_rgba8(128, 128, 128, 178),
Color::from_rgba8(152, 152, 152, 222),
Color::from_rgba8(170, 170, 170, 255),
Color::from_rgba8(255, 255, 255, 102),
)
};
const HOVER_CONTRAST: f32 = 1.8;
const ACTIVE_CONTRAST: f32 = 1.3;
for axis in [AbsoluteAxis::Vertical, AbsoluteAxis::Horizontal] {
if !self.node.wants_scrollbar(axis) {
continue;
}
let Some(thumb) = self.node.scrollbar_thumb(axis) else {
continue;
};
let rect = thumb.scale_from_origin(self.scale);
if let Some(track_color) = custom_track {
let padding_box = self.frame.padding_box;
let track_rect = match axis {
AbsoluteAxis::Horizontal => {
Rect::new(padding_box.x0, rect.y0, padding_box.x1, rect.y1)
}
AbsoluteAxis::Vertical => {
Rect::new(rect.x0, padding_box.y0, rect.x1, padding_box.y1)
}
};
scene.fill(
Fill::NonZero,
self.transform,
track_color.multiply_alpha(opacity),
None,
&track_rect,
);
}
let this = ScrollbarRef { node_id, axis };
let is_active = drag_target == Some(this);
let is_hovered = hovered_thumb == Some(this);
let color = match custom_thumb {
Some(base) if is_active => crate::color::blend_for_contrast(base, ACTIVE_CONTRAST),
Some(base) if is_hovered => crate::color::blend_for_contrast(base, HOVER_CONTRAST),
Some(base) => base,
None if is_active => thumb_active,
None if is_hovered => thumb_hover,
None => thumb_rest,
};
let radius = match axis {
AbsoluteAxis::Horizontal => rect.height() / 2.0,
AbsoluteAxis::Vertical => rect.width() / 2.0,
};
scene.fill(
Fill::NonZero,
self.transform,
color.multiply_alpha(opacity),
None,
&rect.to_rounded_rect(radius),
);
if custom_thumb.is_none() {
let stroke_width = self.scale;
let stroke_rect = rect.inset(-stroke_width / 2.0);
scene.stroke(
&Stroke::new(stroke_width),
self.transform,
stroke_color.multiply_alpha(opacity),
None,
&stroke_rect.to_rounded_rect(radius - stroke_width / 2.0),
);
}
}
}
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);
});
let transform =
self.transform * Affine::translate((pos.x * self.scale, pos.y * self.scale));
crate::text::draw_inline_backgrounds(
scene,
text_layout.layout.lines(),
self.context.dom,
transform,
self.node.id,
);
if let Some(&(sel_start, sel_end)) = self.context.selection_ranges.get(&self.node.id) {
crate::text::draw_text_selection(
scene,
&text_layout.layout,
transform,
sel_start,
sel_end,
);
}
crate::text::stroke_text(
scene,
text_layout.layout.lines(),
self.context.dom,
transform,
self.scale,
);
}
}
fn draw_text_input_text(&self, scene: &mut impl PaintScene, pos: Point) {
if let Some(input_data) = self.text_input {
let y_offset = self.node.text_input_v_centering_offset(self.scale);
let pos = Point {
x: pos.x,
y: pos.y + y_offset,
};
let scroll_offset = input_data.scroll_offset as f64 * self.scale;
let (scroll_x, scroll_y) = if input_data.is_multiline {
(0.0, scroll_offset)
} else {
(scroll_offset, 0.0)
};
let transform = self.transform
* Affine::translate((pos.x * self.scale - scroll_x, pos.y * self.scale - scroll_y));
if self.node.is_focussed() {
for (rect, _line_idx) in input_data.editor.selection_geometry().iter() {
scene.fill(
Fill::NonZero,
transform,
SELECTION_COLOR,
None,
&convert_rect(rect),
);
}
if let Some(cursor) = input_data.editor.cursor_geometry(1.5) {
let color = self.style.get_inherited_text().color;
let caret_color = match &self.style.get_inherited_ui().caret_color.0 {
ColorOrAuto::Auto => color,
ColorOrAuto::Color(caret_color) => caret_color.resolve_to_absolute(&color),
};
scene.fill(
Fill::NonZero,
transform,
caret_color.as_srgb_color(),
None,
&convert_rect(&cursor),
);
};
}
crate::text::stroke_text(
scene,
input_data.editor.try_layout().unwrap().lines(),
self.context.dom,
transform,
self.scale,
);
}
}
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,
};
let transform =
self.transform * Affine::translate((pos.x * self.scale, pos.y * self.scale));
crate::text::stroke_text(
scene,
layout.lines(),
self.context.dom,
transform,
self.scale,
);
}
}
fn draw_children(
&self,
scene: &mut impl PaintScene,
parent_style_transform: Affine,
clip_rect: Rect,
) {
if let Some(hoisted) = &self.node.stacking_context {
for hoisted_child in hoisted.neg_z_hoisted_children() {
let pos = kurbo::Vec2 {
x: hoisted_child.position.x as f64 * self.scale,
y: hoisted_child.position.y as f64 * self.scale,
};
self.render_node(
scene,
hoisted_child.node_id,
parent_style_transform.pre_translate(pos),
clip_rect,
);
}
}
if let Some(children) = &*self.node.paint_children.borrow() {
for child_id in children {
self.render_node(scene, *child_id, parent_style_transform, clip_rect);
}
}
if let Some(hoisted) = &self.node.stacking_context {
for hoisted_child in hoisted.pos_z_hoisted_children() {
let pos = kurbo::Vec2 {
x: hoisted_child.position.x as f64 * self.scale,
y: hoisted_child.position.y as f64 * self.scale,
};
self.render_node(
scene,
hoisted_child.node_id,
parent_style_transform.pre_translate(pos),
clip_rect,
);
}
}
}
#[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 = self
.transform
.pre_scale_non_uniform(x_scale, y_scale)
.then_translate(Vec2 { x, y });
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_translate(Vec2 { x, y })
.pre_scale_non_uniform(x_scale, y_scale);
scene.draw_image(to_peniko_image(image, quality).as_ref(), transform);
}
}
#[cfg(feature = "custom-widget")]
fn draw_custom_widget(&self, scene: &mut impl PaintScene) {
if let Some(widget_scene) = self.custom_widget_scene {
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.append_scene(widget_scene.clone(), transform);
}
}
fn draw_sub_document(&self, scene: &mut impl PaintScene) {
if let Some(sub_doc) = self.element.sub_doc_data().map(|doc| doc.inner()) {
let scale = self.scale;
let width = self.frame.content_box.width() as u32;
let height = self.frame.content_box.height() as u32;
let translation = self.transform.translation();
let initial_x = translation.x + self.frame.content_box.origin().x;
let initial_y = translation.y + self.frame.content_box.origin().y;
let painter = BlitzDomPainter::new(
&sub_doc,
scale,
width,
height,
initial_x,
initial_y,
self.custom_widget_scenes,
);
painter.paint_scene(scene);
}
}
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);
}
}
}
impl<'dom, 'a> std::ops::Deref for ElementCx<'dom, 'a> {
type Target = BlitzDomPainter<'dom, '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.0.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)
}