Images!
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::scene::{PreparedText, Rect, SceneSnapshot, UiSize};
|
||||
use crate::ImageFit;
|
||||
use crate::scene::{PreparedImage, PreparedText, Rect, SceneSnapshot, UiSize};
|
||||
use crate::text::TextSystem;
|
||||
use crate::tree::{CursorIcon, Edges, Element, ElementId, FlexDirection};
|
||||
use crate::tree::{CursorIcon, Edges, Element, ElementId, FlexDirection, ImageNode};
|
||||
|
||||
pub fn layout_scene(version: u64, logical_size: UiSize, root: &Element) -> SceneSnapshot {
|
||||
let mut text_system = TextSystem::new();
|
||||
@@ -60,6 +61,7 @@ pub struct LayoutNode {
|
||||
pub pointer_events: bool,
|
||||
pub focusable: bool,
|
||||
pub cursor: CursorIcon,
|
||||
pub prepared_image: Option<PreparedImage>,
|
||||
pub prepared_text: Option<PreparedText>,
|
||||
pub children: Vec<LayoutNode>,
|
||||
}
|
||||
@@ -188,6 +190,7 @@ fn layout_element(
|
||||
pointer_events: element.style.pointer_events,
|
||||
focusable: element.style.focusable,
|
||||
cursor,
|
||||
prepared_image: None,
|
||||
prepared_text: None,
|
||||
children: Vec::new(),
|
||||
};
|
||||
@@ -223,6 +226,16 @@ fn layout_element(
|
||||
return interaction;
|
||||
}
|
||||
|
||||
if let Some(image) = element.image_node() {
|
||||
let content = inset_rect(rect, element.style.padding);
|
||||
if content.size.width > 0.0 && content.size.height > 0.0 {
|
||||
let prepared = prepare_image(image, content, element.id);
|
||||
scene.push_image(prepared.clone());
|
||||
interaction.prepared_image = Some(prepared);
|
||||
}
|
||||
return interaction;
|
||||
}
|
||||
|
||||
perf_stats.container_nodes += 1;
|
||||
|
||||
if element.children.is_empty() {
|
||||
@@ -427,6 +440,11 @@ fn intrinsic_main_size(
|
||||
return main_axis_size(content, direction) + padding;
|
||||
}
|
||||
|
||||
if let Some(image) = child.image_node() {
|
||||
let resolved = resolve_image_element_size(child, image.resource.size());
|
||||
return main_axis_size(resolved, direction);
|
||||
}
|
||||
|
||||
let available_size = match direction {
|
||||
FlexDirection::Row => UiSize::new(available_main.max(0.0), cross_size.max(0.0)),
|
||||
FlexDirection::Column => UiSize::new(cross_size.max(0.0), available_main.max(0.0)),
|
||||
@@ -461,6 +479,14 @@ fn intrinsic_size(
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(image) = element.image_node() {
|
||||
let resolved = resolve_image_element_size(element, image.resource.size());
|
||||
return UiSize::new(
|
||||
resolved.width + element.style.padding.left + element.style.padding.right,
|
||||
resolved.height + element.style.padding.top + element.style.padding.bottom,
|
||||
);
|
||||
}
|
||||
|
||||
let explicit_width = element.style.width;
|
||||
let explicit_height = element.style.height;
|
||||
let content_width = explicit_width.unwrap_or(available_size.width).max(0.0)
|
||||
@@ -484,9 +510,7 @@ fn intrinsic_size(
|
||||
let mut width: f32 = 0.0;
|
||||
let mut height = gap_total;
|
||||
for child in &element.children {
|
||||
if child.style.flex_grow > 0.0 && child.style.height.is_none() {
|
||||
continue;
|
||||
}
|
||||
let skip_main = child.style.flex_grow > 0.0 && child.style.height.is_none();
|
||||
let child_size = intrinsic_size(
|
||||
child,
|
||||
UiSize::new(
|
||||
@@ -497,15 +521,23 @@ fn intrinsic_size(
|
||||
perf_stats,
|
||||
);
|
||||
width = width.max(child.style.width.unwrap_or(child_size.width));
|
||||
height += child.style.height.unwrap_or(child_size.height);
|
||||
if !skip_main {
|
||||
height += child.style.height.unwrap_or(child_size.height);
|
||||
}
|
||||
}
|
||||
(width, height)
|
||||
}
|
||||
FlexDirection::Row => {
|
||||
let mut width = gap_total;
|
||||
let mut height: f32 = 0.0;
|
||||
let mut fixed_main = 0.0;
|
||||
let mut flex_total = 0.0;
|
||||
let mut child_main_sizes = Vec::with_capacity(element.children.len());
|
||||
for child in &element.children {
|
||||
if child.style.flex_grow > 0.0 && child.style.width.is_none() {
|
||||
let is_flex = child.style.flex_grow > 0.0 && child.style.width.is_none();
|
||||
if is_flex {
|
||||
flex_total += child_flex_weight(child);
|
||||
child_main_sizes.push(None);
|
||||
continue;
|
||||
}
|
||||
let child_size = intrinsic_size(
|
||||
@@ -517,7 +549,32 @@ fn intrinsic_size(
|
||||
text_system,
|
||||
perf_stats,
|
||||
);
|
||||
width += child.style.width.unwrap_or(child_size.width);
|
||||
let child_main = child.style.width.unwrap_or(child_size.width);
|
||||
fixed_main += child_main;
|
||||
child_main_sizes.push(Some(child_main));
|
||||
}
|
||||
let remaining_main = (content_size.width - gap_total - fixed_main).max(0.0);
|
||||
for (child, measured_main) in element.children.iter().zip(child_main_sizes.iter()) {
|
||||
let skip_main = child.style.flex_grow > 0.0 && child.style.width.is_none();
|
||||
let child_main = measured_main.unwrap_or_else(|| {
|
||||
if flex_total <= 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
remaining_main * (child_flex_weight(child) / flex_total)
|
||||
}
|
||||
});
|
||||
let child_size = intrinsic_size(
|
||||
child,
|
||||
UiSize::new(
|
||||
child_main,
|
||||
child.style.height.unwrap_or(content_size.height),
|
||||
),
|
||||
text_system,
|
||||
perf_stats,
|
||||
);
|
||||
if !skip_main {
|
||||
width += child_main;
|
||||
}
|
||||
height = height.max(child.style.height.unwrap_or(child_size.height));
|
||||
}
|
||||
(width, height)
|
||||
@@ -569,6 +626,67 @@ impl LayoutPerfStats {
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_image(image: &ImageNode, rect: Rect, element_id: Option<ElementId>) -> PreparedImage {
|
||||
let source_size = image.resource.size();
|
||||
let source_aspect = if source_size.height > 0.0 {
|
||||
source_size.width / source_size.height
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let rect_aspect = if rect.size.height > 0.0 {
|
||||
rect.size.width / rect.size.height
|
||||
} else {
|
||||
source_aspect
|
||||
};
|
||||
|
||||
let (draw_rect, uv_rect) = match image.fit {
|
||||
ImageFit::Fill => (rect, (0.0, 0.0, 1.0, 1.0)),
|
||||
ImageFit::Contain => {
|
||||
let scale = (rect.size.width / source_size.width)
|
||||
.min(rect.size.height / source_size.height)
|
||||
.max(0.0);
|
||||
let width = source_size.width * scale;
|
||||
let height = source_size.height * scale;
|
||||
let x = rect.origin.x + (rect.size.width - width) * 0.5;
|
||||
let y = rect.origin.y + (rect.size.height - height) * 0.5;
|
||||
(Rect::new(x, y, width, height), (0.0, 0.0, 1.0, 1.0))
|
||||
}
|
||||
ImageFit::Cover => {
|
||||
if rect_aspect > source_aspect {
|
||||
let visible_height = (source_aspect / rect_aspect).clamp(0.0, 1.0);
|
||||
let inset = (1.0 - visible_height) * 0.5;
|
||||
(rect, (0.0, inset, 1.0, 1.0 - inset))
|
||||
} else {
|
||||
let visible_width = (rect_aspect / source_aspect).clamp(0.0, 1.0);
|
||||
let inset = (1.0 - visible_width) * 0.5;
|
||||
(rect, (inset, 0.0, 1.0 - inset, 1.0))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
PreparedImage {
|
||||
element_id,
|
||||
resource: image.resource.clone(),
|
||||
rect: draw_rect,
|
||||
uv_rect,
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_image_element_size(element: &Element, intrinsic: UiSize) -> UiSize {
|
||||
match (element.style.width, element.style.height) {
|
||||
(Some(width), Some(height)) => UiSize::new(width.max(0.0), height.max(0.0)),
|
||||
(Some(width), None) if intrinsic.width > 0.0 => UiSize::new(
|
||||
width.max(0.0),
|
||||
(width * intrinsic.height / intrinsic.width).max(0.0),
|
||||
),
|
||||
(None, Some(height)) if intrinsic.height > 0.0 => UiSize::new(
|
||||
(height * intrinsic.width / intrinsic.height).max(0.0),
|
||||
height.max(0.0),
|
||||
),
|
||||
_ => intrinsic,
|
||||
}
|
||||
}
|
||||
|
||||
fn inset_rect(rect: Rect, edges: Edges) -> Rect {
|
||||
let width = (rect.size.width - edges.left - edges.right).max(0.0);
|
||||
let height = (rect.size.height - edges.top - edges.bottom).max(0.0);
|
||||
@@ -773,6 +891,60 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn row_container_counts_flex_child_height_in_intrinsic_size() {
|
||||
let row_color = Color::rgb(0x12, 0x24, 0x36);
|
||||
let root = Element::column().child(
|
||||
Element::row()
|
||||
.padding(Edges::all(12.0))
|
||||
.gap(16.0)
|
||||
.background(row_color)
|
||||
.children([
|
||||
Element::new()
|
||||
.width(120.0)
|
||||
.height(60.0)
|
||||
.background(Color::rgb(0x44, 0x55, 0x66)),
|
||||
Element::column().flex(1.0).child(Element::paragraph(
|
||||
"A flex child with wrapped text should make the row grow tall enough to contain it.",
|
||||
TextStyle::new(18.0, Color::rgb(0xFF, 0xFF, 0xFF))
|
||||
.with_line_height(24.0)
|
||||
.with_wrap(TextWrap::Word),
|
||||
)),
|
||||
]),
|
||||
);
|
||||
|
||||
let scene = layout_scene(1, UiSize::new(360.0, 320.0), &root);
|
||||
let row_quad = scene
|
||||
.items
|
||||
.iter()
|
||||
.filter_map(|item| match item {
|
||||
DisplayItem::Quad(quad) if quad.color == row_color => Some(*quad),
|
||||
_ => None,
|
||||
})
|
||||
.next()
|
||||
.expect("row container should emit a background quad");
|
||||
let text = scene
|
||||
.items
|
||||
.iter()
|
||||
.find_map(|item| match item {
|
||||
DisplayItem::Text(text) => Some(text),
|
||||
_ => None,
|
||||
})
|
||||
.expect("row should emit a text display item");
|
||||
let text_bounds = text.bounds.expect("text layout should provide bounds");
|
||||
let row_bottom = row_quad.rect.origin.y + row_quad.rect.size.height;
|
||||
let text_bottom = text.origin.y + text_bounds.height;
|
||||
|
||||
assert!(row_quad.rect.size.height > 84.0);
|
||||
assert!(row_bottom >= text_bottom);
|
||||
assert!(
|
||||
scene
|
||||
.items
|
||||
.iter()
|
||||
.any(|item| matches!(item, DisplayItem::Text(_)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interaction_tree_hit_test_returns_deepest_pointer_target() {
|
||||
let root = Element::column()
|
||||
|
||||
Reference in New Issue
Block a user