Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/fixed_vector_container_paint_bounds.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
default: major
---

# Fixed vector containers clipping their children's overflow

`VectorLayer`, `Frame`, `Flex`, and `Padding` reported only their own layout box as their paint bounds, so anything a child painted outside that box — an outset stroke, a drop shadow, an overhanging chip — was cut off during rasterization. They now start from the layout box and grow it to include their children's paint bounds, matching the raster variants of the same name.

Existing projects whose layouts contain such overflow may produce different video pixels after upgrading, so regenerate and review reference renders or snapshots.
2 changes: 1 addition & 1 deletion docs/layout-tutorial.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,6 @@ use tellur_core::layout::{Frame, Flex, Flexible, Stack}; // vector
use tellur_core::layout::raster::{Frame, Flex, Flexible, Stack}; // raster
```

ソース上も 1 コンテナ 1 ファイル(`layout/frame.rs` など)に両変種が同居しています。raster 側だけの追加責務は `paint_bounds`(ドロップシャドウ等のはみ出しを含む描画範囲)と、キャッシュとの付き合い方(`Flexible` や `Positioned` は `CachePolicy::Transparent` で子にキャッシュスロットを譲る)です。
ソース上も 1 コンテナ 1 ファイル(`layout/frame.rs` など)に両変種が同居しています。`paint_bounds`(外側にはみ出すストロークやドロップシャドウ等を含む描画範囲)は**両変種の共通責務**です。同じツリーなら両者は同じ矩形を返さねばならず、vector 側はさらにその矩形を `render().view_box` としても返す必要があります。したがって子をレイアウトするコンテナは、自身の `(0,0)..size` から出発して子の paint bounds を union します。例外は意図的に自分の箱へ pin するコンテナで、`DecoratedBox` がクリップ矩形として働くのはこのためです。raster 側だけの追加責務は、キャッシュとの付き合い方(`Flexible` や `Positioned` は `CachePolicy::Transparent` で子にキャッシュスロットを譲る)だけです。

例外が2つ: `Clip` は vector 専用(raster で欲しければ切ってから `.rasterize()`)、逆に `.opacity()` は raster 側にも同名で存在します(= `Opacity`。alpha 乗算のみで、`.transform()` / `.transform_around()` は今も vector 専用です)。
2 changes: 1 addition & 1 deletion docs/layout-tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,6 @@ use tellur_core::layout::{Frame, Flex, Flexible, Stack}; // vector
use tellur_core::layout::raster::{Frame, Flex, Flexible, Stack}; // raster
```

In the source, both variants live in one file per container (`layout/frame.rs` etc.). The raster side's only extra responsibilities are `paint_bounds` (the drawn extent including overflow such as drop shadows) and its relationship with the cache (`Flexible` and `Positioned` use `CachePolicy::Transparent` to hand their cache slot to the child).
In the source, both variants live in one file per container (`layout/frame.rs` etc.). `paint_bounds` — the drawn extent, including overflow such as an outset stroke or a drop shadow — is a **shared** responsibility: for the same tree both variants must report the same rectangle, and the vector variant must return that same rectangle as its `render().view_box`. A container that lays out children therefore starts from its own `(0,0)..size` box and unions in every child's paint bounds; the exception is a container that pins its bounds to its own box on purpose, which is what makes `DecoratedBox` a clip rectangle. The raster side's only extra responsibility is its relationship with the cache (`Flexible` and `Positioned` use `CachePolicy::Transparent` to hand their cache slot to the child).

Two exceptions: `Clip` is vector-only (for raster, cut first, then `.rasterize()`), and conversely `.opacity()` also exists on the raster side under the same name (= `Opacity`; alpha multiplication only — `.transform()` / `.transform_around()` remain vector-only).
20 changes: 16 additions & 4 deletions tellur-core/src/layer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,25 @@ impl VectorComponent for VectorLayer {
constraints.constrain(self.size)
}

fn paint_bounds(&self, size: Vec2) -> Rect {
// Start from the `(0,0)..size` rect and grow it to include any
// children that overflow the box. Mirrors the raster [`Layer`].
let child_constraints = Constraints::loose(size);
let mut bounds = Rect {
origin: Vec2::ZERO,
size,
};
for child in &self.children {
let child_size = child.layout(child_constraints);
bounds = union_rect(bounds, child.paint_bounds(child_size));
}
bounds
}

fn render(&self, size: Vec2) -> VectorGraphic {
render_vector_children(
&self.children,
Rect {
origin: Vec2::ZERO,
size,
},
self.paint_bounds(size),
Constraints::loose(size),
)
}
Expand Down
47 changes: 34 additions & 13 deletions tellur-core/src/layout/flex.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! [`Flex`]: flexbox-style arrangement of children along one axis.

use crate::geometry::{Axis, Constraints, Rect, Transform, Vec2};
use crate::layer::{translate_rect, union_rect};
use crate::vector::{Group, Node, VectorComponent, VectorGraphic};
use crate::Keyable;

Expand Down Expand Up @@ -74,6 +75,33 @@ impl Flex {
})
.collect()
}

/// The child arrangement at an already-resolved `size`. Shared by
/// `paint_bounds` and `render` so both read the same placements.
fn resolved_pass(&self, size: Vec2) -> FlexPass {
compute_flex_pass(
self.axis,
self.spacing,
self.main_align,
self.cross_align,
Constraints::tight(size),
&self.child_grows(),
|i, c| self.children[i].layout(c),
)
}

/// The flex box grown to include any child paint that overflows it.
/// Mirrors the raster [`Flex`](raster::Flex).
fn pass_bounds(&self, pass: &FlexPass) -> Rect {
let mut bounds = Rect {
origin: Vec2::ZERO,
size: pass.own_size,
};
for (child, &(pos, child_size)) in self.children.iter().zip(pass.children.iter()) {
bounds = union_rect(bounds, translate_rect(child.paint_bounds(child_size), pos));
}
bounds
}
}

pub(super) struct FlexPass {
Expand Down Expand Up @@ -252,16 +280,12 @@ impl VectorComponent for Flex {
.own_size
}

fn paint_bounds(&self, size: Vec2) -> Rect {
self.pass_bounds(&self.resolved_pass(size))
}

fn render(&self, size: Vec2) -> VectorGraphic {
let pass = compute_flex_pass(
self.axis,
self.spacing,
self.main_align,
self.cross_align,
Constraints::tight(size),
&self.child_grows(),
|i, c| self.children[i].layout(c),
);
let pass = self.resolved_pass(size);
let nodes: Vec<Node> = self
.children
.iter()
Expand All @@ -276,10 +300,7 @@ impl VectorComponent for Flex {
})
.collect();
VectorGraphic {
view_box: Rect {
origin: Vec2::ZERO,
size: pass.own_size,
},
view_box: self.pass_bounds(&pass),
root: Node::Group(Group {
transform: Transform::IDENTITY,
opacity: 1.0,
Expand Down
19 changes: 15 additions & 4 deletions tellur-core/src/layout/frame.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! [`Frame`]: per-axis sizing for one top-left-aligned child.

use crate::geometry::{Constraints, Rect, Transform, Vec2};
use crate::layer::union_rect;
use crate::vector::{Group, Node, VectorComponent, VectorGraphic};
use crate::Keyable;

Expand Down Expand Up @@ -69,14 +70,24 @@ impl VectorComponent for Frame {
})
}

fn render(&self, size: Vec2) -> VectorGraphic {
fn paint_bounds(&self, size: Vec2) -> Rect {
// Start from the `(0,0)..size` rect and grow it to include a child
// that overflows the box. Mirrors the raster [`Frame`](raster::Frame).
let child_size = self.child.layout(Constraints::loose(size));
let inner = self.child.render(child_size);
VectorGraphic {
view_box: Rect {
union_rect(
Rect {
origin: Vec2::ZERO,
size,
},
self.child.paint_bounds(child_size),
)
}

fn render(&self, size: Vec2) -> VectorGraphic {
let child_size = self.child.layout(Constraints::loose(size));
let inner = self.child.render(child_size);
VectorGraphic {
view_box: self.paint_bounds(size),
root: Node::Group(Group {
transform: Transform::IDENTITY,
opacity: 1.0,
Expand Down
27 changes: 21 additions & 6 deletions tellur-core/src/layout/padding.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! [`Padding`]: empty space around a child.

use crate::geometry::{Constraints, EdgeInsets, Rect, Transform, Vec2};
use crate::layer::{translate_rect, union_rect};
use crate::vector::{Group, Node, VectorComponent, VectorGraphic};
use crate::Keyable;

Expand All @@ -17,6 +18,11 @@ impl Padding {
fn inset_size(&self) -> Vec2 {
Vec2(self.insets.horizontal(), self.insets.vertical())
}

fn inner_size(&self, size: Vec2) -> Vec2 {
let inset = self.inset_size();
Vec2((size.0 - inset.0).max(0.0), (size.1 - inset.1).max(0.0))
}
}

impl VectorComponent for Padding {
Expand All @@ -26,15 +32,24 @@ impl VectorComponent for Padding {
Vec2(child_size.0 + inset.0, child_size.1 + inset.1)
}

fn render(&self, size: Vec2) -> VectorGraphic {
let inset = self.inset_size();
let inner_size = Vec2((size.0 - inset.0).max(0.0), (size.1 - inset.1).max(0.0));
let inner = self.child.render(inner_size);
VectorGraphic {
view_box: Rect {
fn paint_bounds(&self, size: Vec2) -> Rect {
// Start from the `(0,0)..size` rect and grow it to include a child
// that overflows the padded box. Mirrors the raster
// [`Padding`](raster::Padding).
let child_paint = self.child.paint_bounds(self.inner_size(size));
union_rect(
Rect {
origin: Vec2::ZERO,
size,
},
translate_rect(child_paint, self.insets.top_left()),
)
}

fn render(&self, size: Vec2) -> VectorGraphic {
let inner = self.child.render(self.inner_size(size));
VectorGraphic {
view_box: self.paint_bounds(size),
root: Node::Group(Group {
transform: Transform::translate(self.insets.top_left()),
opacity: 1.0,
Expand Down
Loading