feat: constrain flex bases and redistribute bounded growth and shrink
Verify Tessera GUI / verify (push) Failing after 9m4s

This commit is contained in:
2026-10-23 00:42:48 +08:00
parent 78fb66277d
commit 1fda07ebc5
12 changed files with 298 additions and 55 deletions
+5 -2
View File
@@ -262,7 +262,7 @@ Native TextBlock measurement now uses the occurrence's maximum width, with word
Native TextBlock painting carries its allocated width/height into the immutable frame and clips glyphs to those bounds as well as any enclosing scroll clip. Measurement and drawing use the same font/shaping implementation; drawing shapes at the device font size. Zero-width or zero-height paint bounds draw nothing. Management UI text keeps its existing single-line/explicit-newline behavior.
Source `PaintCommand` text may supply both `wrapWidth` and `wrapHeight` as finite nonnegative logical dimensions up to 1000000. Omitting both preserves unwrapped text; supplying only one is rejected. `TextBlock.paint` returns these fields, so a source override can preserve or explicitly change the boundary. Changing paint bounds does not renegotiate layout: source arrange supplies child Bounds and changed dimensions participate in the layout solve. General flex basis/min/max and fixed-child shrinking remain pending. ScrollBlock's axis policy now controls cross-axis wrapping and scrolling. Fractional-DPI wrap boundaries need broader platform validation.
Source `PaintCommand` text may supply both `wrapWidth` and `wrapHeight` as finite nonnegative logical dimensions up to 1000000. Omitting both preserves unwrapped text; supplying only one is rejected. `TextBlock.paint` returns these fields, so a source override can preserve or explicitly change the boundary. Changing paint bounds does not renegotiate layout: source arrange supplies child Bounds and changed dimensions participate in the layout solve. Bounded flex allocation is described below. ScrollBlock's axis policy now controls cross-axis wrapping and scrolling. Fractional-DPI wrap boundaries need broader platform validation.
A source subclass can override `paint(self, mount, geometry): DisplayList`. The renderer invokes that method for each occurrence, after Mount synchronization and hooks, using current logical Bounds. Its commands replace that occurrence's native paint in preorder. Calling `RectBlock.paint(self, mount, geometry)` or another native base method obtains a fresh result that can be modified before returning. `DisplayList(ObjectList(command, ...))` builds multiple commands; ObjectList accepts objects and has a 10000-member bound. PaintCommand fields use kind (`roundedRect`, `strokeRoundedRect` or `text`), x/y/color and the corresponding width/height/radius, strokeWidth, or text/size fields. Commands must pass the same native validation and a total 10000-command frame limit. See [source-paint.endor](examples/source-paint.endor), including a Button source method that updates the same tint field used by two occurrences.
@@ -490,4 +490,7 @@ Event dispatch/click and lifecycle entry points publish External as their allowe
ContainerBlock.mainAlign is a managed attribute with start (default), center, end and space-between policies. It positions children in the allocated main-axis space after padding and the configured minimum gap. Center/end use remaining positive space; space-between adds it between two or more children. Overflow keeps start positions and existing sizes; alignment does not imply flex sizing. Native arrange queries and rendering use the same actual Bounds. Source arrange overrides retain their explicit placements.
Block.flex is a nonnegative managed weight, default 0. In native containers, fixed children retain measured main-axis sizes; positive weights share the remaining allocated space after padding/gaps with zero basis. This does not request extra parent size. Actual allocated child bounds feed back into measurement before publication. Source arrangement overrides remain authoritative. General flex basis/min/max and fixed-child shrinking are not implemented yet. See [flex.endor](examples/flex.endor) for shared text reflow and native button updates.
Block.flex is a nonnegative managed weight, default 0. In native containers, fixed children retain measured main-axis sizes; positive weights share the remaining allocated space after padding/gaps with zero basis. This does not request extra parent size. Actual allocated child bounds feed back into measurement before publication. Source arrangement overrides remain authoritative. The additional flexBasis, flexMin, flexMax and flexShrink attributes control bounded allocation and opt-in shrinking along the parent main axis; see the policy below. See [flex.endor](examples/flex.endor) for shared text reflow and native button updates.
Flex main-axis policy: flexBasis is "auto" by default (zero basis when flex > 0, otherwise the first measured size of this occurrence in the current solve); a number supplies an explicit basis. flexMin defaults to 0, flexMax to 1000000, and flexShrink to 0. All numbers are finite/nonnegative and at most 1000000; flexMin must not exceed flexMax. The parent clamps bases to min/max, distributes positive space by flex, and redistributes space left after a maximum is reached. Deficits shrink by flexShrink times the original basis, freezing at minima. A fixed child can opt into shrink without growth. Unresolvable minimum overflow is preserved, and remaining positive space follows mainAlign. The policy applies to width in a Row and height in Column-like containers; these are not independent width/height bounds. Source arrange overrides remain authoritative. Preferred sizes are derived per occurrence and reset for each new solve, never shared through Block state. See examples/flex-constraints.endor. Layout continuation remains unfinished.
+40
View File
@@ -0,0 +1,40 @@
class Strip: Row {
function measure(self: Strip, mount: Mount, constraints: Constraints): MeasureResult {
return MeasureResult(340, 60)
}
}
class Actions {
first: RectBlock
second: RectBlock
function grow(self: Actions): Unit {
self.first.flexMin = 0
self.first.flexMax = 105
self.first.flexBasis = 100
self.second.flexBasis = 100
self.first.flex = 1
self.second.flex = 3
}
function shrink(self: Actions): Unit {
self.first.flexMax = 300
self.first.flexMin = 150
self.first.flexBasis = 200
self.second.flexBasis = 200
self.first.flex = 0
self.second.flex = 0
self.first.flexShrink = 1
self.second.flexShrink = 1
}
}
first = RectBlock(20, 20, 4, "#A4C8F0")
second = RectBlock(40, 20, 4, "#A6D8B2")
fixed = RectBlock(60, 20, 4, "#E8C990")
actions = Actions(first, second)
grow = Button("增长:上限后重新分配")
grow.onClick(actions.grow)
shrink = Button("收缩:最小尺寸后重新分配")
shrink.onClick(actions.shrink)
actions.grow()
strip = Strip(10, 10, first, second, fixed)
page = Page()
page.title = "基础尺寸与边界"
page.render(Column(20, 12, Row(0, 10, grow, shrink), strip, TextBlock("增长:105 / 135 / 60;收缩:150 / 90 / 60。")))
+4 -1
View File
@@ -140,6 +140,9 @@ ScrollBlock.axis is stored in its managed properties and projected into the visu
Container mainAlign projects from managed properties into UiNode and its shallow equality. Native arrangement receives the actual allocated Size, computes positive remaining main-axis space and keeps child sizes unchanged.
Block.flex is a nonnegative managed weight, default 0. In native containers, fixed children retain measured main-axis sizes; positive weights share the remaining allocated space after padding/gaps with zero basis. This does not request extra parent size. Actual allocated child bounds feed back into measurement before publication. Source arrangement overrides remain authoritative. General flex basis/min/max and fixed-child shrinking are not implemented yet.
Block.flex is a nonnegative managed weight, default 0. In native containers, fixed children retain measured main-axis sizes; positive weights share the remaining allocated space after padding/gaps with zero basis. This does not request extra parent size. Actual allocated child bounds feed back into measurement before publication. Source arrangement overrides remain authoritative. The additional flexBasis, flexMin, flexMax and flexShrink attributes control bounded allocation and opt-in shrinking along the parent main axis; see the policy below.
Trees with positive flex weights use the settling driver even when all layout methods are native. This remeasures wrapping and ancestor geometry before publishing. Fast-path eligibility checks actual intrinsic implementations, rather than comparing against a mutable Block method table.
Flex main-axis policy: flexBasis is "auto" by default (zero basis when flex > 0, otherwise the first measured size of this occurrence in the current solve); a number supplies an explicit basis. flexMin defaults to 0, flexMax to 1000000, and flexShrink to 0. All numbers are finite/nonnegative and at most 1000000; flexMin must not exceed flexMax. The parent clamps bases to min/max, distributes positive space by flex, and redistributes space left after a maximum is reached. Deficits shrink by flexShrink times the original basis, freezing at minima. A fixed child can opt into shrink without growth. Unresolvable minimum overflow is preserved, and remaining positive space follows mainAlign. The policy applies to width in a Row and height in Column-like containers; these are not independent width/height bounds. Source arrange overrides remain authoritative. Preferred sizes are derived per occurrence and reset for each new solve, never shared through Block state. See examples/flex-constraints.endor. Layout continuation remains unfinished.
+6 -3
View File
@@ -1085,9 +1085,9 @@ ContainerBlock.insert(position,block) accepts a zero-based position through the
Source measure and arrange overrides execute bottom-up and top-down in one read-only VM layout call. Structural Mount links exist before first measurement; bounds queries reject premature geometry. Native base layout uses current occurrence child measurements, and Arrangement must assign every stable child slot exactly once. Layout dependencies changed by lifecycle hooks trigger another pass before publication. Page constraints now use the current logical window viewport. Column/Button propagate cross-axis width and Row propagates cross-axis height; desired sizes are clamped. ui.viewport() provides a managed snapshot whose private input participates in rendering dependencies. Native resize updates are coalesced on the same worker without adding source inputs. Source measurement caches use Mount/method/children/constraints and actual subcall object reads, including returned-result decoding; Mount-reading methods conservatively bypass caching. ui.stats() exposes sourceMeasureHits/sourceMeasureMisses outside read-only rendering. Native TextBlock wrapping now uses the occurrence width in measurement/cache/painting; source measurement/arrangement now repeats until constraints and geometry settle; additional allocation policies remain pending; ScrollBlock scrolling is described below.
Source layout solves repeat in one read-only call until constraints/sizes/bounds are unchanged, reusing one bytecode frame. Source-arranged changed dimensions become exact per-axis constraints for this solve; source container measurements differing from native results constrain content using the same container policy. Selected limits reset on later inputs/resizes. Nonconsecutive repeated states report a cycle. No fixed pass/hop cutoff is used; all passes share the 1000000-instruction/128-frame budget and budget failure is explicit. Measurement clears provisional Mount geometry, and only complete settled frames publish. ui.stats().layoutPasses reports work; source/cache counters include all passes. VM annotation checks distinguish validation-only from content reads, preserving Mount-typed independent measurement caching; actual Mount reads still bypass conservatively. General flex allocation and resumable solving remain pending. See examples/layout-negotiation.endor.
Source layout solves repeat in one read-only call until constraints/sizes/bounds are unchanged, reusing one bytecode frame. Source-arranged changed dimensions become exact per-axis constraints for this solve; source container measurements differing from native results constrain content using the same container policy. Selected limits reset on later inputs/resizes. Nonconsecutive repeated states report a cycle. No fixed pass/hop cutoff is used; all passes share the 1000000-instruction/128-frame budget and budget failure is explicit. Measurement clears provisional Mount geometry, and only complete settled frames publish. ui.stats().layoutPasses reports work; source/cache counters include all passes. VM annotation checks distinguish validation-only from content reads, preserving Mount-typed independent measurement caching; actual Mount reads still bypass conservatively. Resumable solving remains pending. See examples/layout-negotiation.endor.
TextBlock now wraps words/glyphs at its available width and measures the resulting height. Native and source-base measurement paths use current constraints; native shape/font caches include wrap width. Painting carries allocated width/height, clips glyphs and keeps ancestor scroll clipping. Source text PaintCommand may supply both wrapWidth/wrapHeight (finite nonnegative logical dimensions up to 1000000); neither preserves legacy unwrapped paint, and a partial pair is rejected. Native TextBlock.paint supplies the pair. These are derived draw bounds, not another business object. Management UI text remains unwrapped. General flex basis/min/max, fixed-child shrinking, resumable solving and broad fractional-DPI validation remain pending. See examples/wrapped-text.endor.
TextBlock now wraps words/glyphs at its available width and measures the resulting height. Native and source-base measurement paths use current constraints; native shape/font caches include wrap width. Painting carries allocated width/height, clips glyphs and keeps ancestor scroll clipping. Source text PaintCommand may supply both wrapWidth/wrapHeight (finite nonnegative logical dimensions up to 1000000); neither preserves legacy unwrapped paint, and a partial pair is rejected. Native TextBlock.paint supplies the pair. These are derived draw bounds, not another business object. Management UI text remains unwrapped. Resumable solving and broad fractional-DPI validation remain pending. See examples/wrapped-text.endor.
Source paint overrides execute in the actual frame pipeline under a read-only heap policy. Source hitTest overrides similarly refine native pointer targets in VM paint order; Point.inside(bounds) and Point.inCircle(cx, cy, radius) provide primitive shape queries. Managed DisplayList/ObjectList/PaintCommand and Constraints/Bounds/Point results are ordinary runtime objects.
@@ -1119,7 +1119,10 @@ ScrollBlock.axis is a managed visual attribute: "both" (default), "vertical" or
ContainerBlock.mainAlign accepts "start" (default), "center", "end" or "space-between". Native arrangement uses allocated Bounds after padding/minimum gap, distributes positive remaining main-axis space and keeps child sizes unchanged. Overflow keeps start placement. Explicit source arrange placements remain authoritative; this is alignment, not flex sizing.
Block.flex is a nonnegative weight (default 0). Native containers reserve fixed measured children, padding and gaps, then share remaining allocated main-axis space between positive weights with zero basis. Actual child allocation feeds remeasurement. It does not request parent expansion or implement general flex basis/min/max/fixed-child shrinking.
Block.flex is a nonnegative weight (default 0). Native containers reserve fixed measured children, padding and gaps, then share remaining allocated main-axis space between positive weights with zero basis. Actual child allocation feeds remeasurement. It does not request parent expansion. Further controls are described below.
Flex main-axis policy: flexBasis is "auto" by default (zero basis when flex > 0, otherwise the first measured size of this occurrence in the current solve); a number supplies an explicit basis. flexMin defaults to 0, flexMax to 1000000, and flexShrink to 0. All numbers are finite/nonnegative and at most 1000000; flexMin must not exceed flexMax. The parent clamps bases to min/max, distributes positive space by flex, and redistributes space left after a maximum is reached. Deficits shrink by flexShrink times the original basis, freezing at minima. A fixed child can opt into shrink without growth. Unresolvable minimum overflow is preserved, and remaining positive space follows mainAlign. The policy applies to width in a Row and height in Column-like containers; these are not independent width/height bounds. Source arrange overrides remain authoritative. Preferred sizes are derived per occurrence and reset for each new solve, never shared through Block state. See examples/flex-constraints.endor. Layout continuation remains unfinished.
## UI host object
+42 -1
View File
@@ -118,6 +118,7 @@ pub struct Data {
pub padding: f32,
pub gap: f32,
pub flex: f32,
pub flex_policy: crate::layout::FlexPolicy,
pub main_align: crate::layout::MainAlign,
pub scroll_axis: crate::scroll::Axis,
pub next_slot: u64,
@@ -146,6 +147,7 @@ impl Data {
padding,
gap,
flex: 0.0,
flex_policy: crate::layout::FlexPolicy::default(),
main_align: crate::layout::MainAlign::Start,
scroll_axis: crate::scroll::Axis::Both,
next_slot,
@@ -317,7 +319,18 @@ pub fn native_properties(data: &Data) -> Vec<(&'static str, Value)> {
fn number(value: f32) -> Value {
Value::Number((value as f64).into())
}
let mut fields = vec![("flex", number(data.flex))];
let mut fields = vec![
("flex", number(data.flex)),
(
"flexBasis",
data.flex_policy
.basis
.map_or_else(|| Value::Text("auto".into()), number),
),
("flexMin", number(data.flex_policy.min)),
("flexMax", number(data.flex_policy.max)),
("flexShrink", number(data.flex_policy.shrink)),
];
match &data.kind {
Kind::Text { value, size, color } => fields.extend([
("text", Value::Text(value.clone())),
@@ -362,6 +375,9 @@ pub fn visual_data(store: &ObjectStore, data: &Data) -> Result<Data, VmError> {
let value = store.get_attr(properties, name)?;
apply_visual_property(&mut data, name, &value)?;
}
if data.flex_policy.min > data.flex_policy.max {
return Err(VmError::InvalidArgument("flexMin exceeds flexMax"));
}
Ok(data)
}
@@ -377,6 +393,31 @@ pub fn apply_visual_property(data: &mut Data, name: &str, value: &Value) -> Resu
}
Ok(n)
};
match name {
"flexBasis" => {
data.flex_policy.basis = if matches!(value, Value::Nil)
|| matches!(value, Value::Text(text) if text == "auto")
{
None
} else {
Some(number()?)
};
return Ok(());
}
"flexMin" => {
data.flex_policy.min = number()?;
return Ok(());
}
"flexMax" => {
data.flex_policy.max = number()?;
return Ok(());
}
"flexShrink" => {
data.flex_policy.shrink = number()?;
return Ok(());
}
_ => {}
}
if name == "flex" {
data.flex = number()?;
return Ok(());
+48
View File
@@ -3889,6 +3889,54 @@ page.render(block)
);
}
#[test]
fn bounded_flex_redistributes_growth_and_scaled_shrink() {
let (mut session, initial) = ReplSession::load(
r#"
class Wide: Row {
function measure(self: Wide, mount: Mount, constraints: Constraints): MeasureResult { return MeasureResult(340, 60) }
}
first = RectBlock(20, 20)
second = RectBlock(40, 20)
fixed = RectBlock(60, 20)
first.flex = 1
second.flex = 3
first.flexBasis = 100
second.flexBasis = 100
first.flexMax = 105
row = Wide(10, 10, first, second, fixed)
page = Page()
page.render(row)
"#,
"app.main",
267,
1,
)
.unwrap();
initial.validate().unwrap();
let width = |session: &mut ReplSession, name| {
session
.execute(&format!("{name}.mounts().at(0).bounds().width"), "app.main")
.unwrap()
.value
};
assert_eq!(width(&mut session, "first"), "105");
assert_eq!(width(&mut session, "second"), "135");
session.execute(
"first.flex = 0\nsecond.flex = 0\nfirst.flexMax = 300\nfirst.flexMin = 150\nfirst.flexBasis = 200\nsecond.flexBasis = 200\nfirst.flexShrink = 1\nsecond.flexShrink = 1",
"app.main",
).unwrap();
assert_eq!(width(&mut session, "first"), "150");
assert_eq!(width(&mut session, "second"), "90");
assert_eq!(width(&mut session, "fixed"), "60");
session.execute(
"first.flexMin = 0\nfirst.flexMax = 1000000\nfirst.flexBasis = \"auto\"\nsecond.flexBasis = \"auto\"",
"app.main",
).unwrap();
assert_eq!(width(&mut session, "first"), "20");
assert_eq!(width(&mut session, "second"), "40");
}
#[test]
fn native_flex_reflows_shared_text_per_occurrence() {
let (mut session, initial) = ReplSession::load(
+116 -37
View File
@@ -40,6 +40,25 @@ impl MainAlign {
}
}
/// Main-axis constraints interpreted independently by each parent occurrence.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FlexPolicy {
pub basis: Option<f32>,
pub min: f32,
pub max: f32,
pub shrink: f32,
}
impl Default for FlexPolicy {
fn default() -> Self {
Self {
basis: None,
min: 0.0,
max: MAX_DIMENSION as f32,
shrink: 0.0,
}
}
}
#[derive(Default, Debug, Clone, Copy, PartialEq)]
pub struct Size {
pub width: f32,
@@ -226,6 +245,7 @@ pub fn measure_native(node: &UiNode, children: Vec<Size>, constraints: [f32; 4])
pub struct Context {
pub node: Option<std::sync::Arc<UiNode>>,
pub children: Vec<Size>,
pub preferred: Vec<Size>,
pub bounds: Option<[f32; 4]>,
}
@@ -260,50 +280,109 @@ pub fn paint_native(node: &UiNode, x: f32, y: f32, size: Size) -> Option<RenderC
}
}
/// Zero-basis weighted allocation within the space actually assigned to a
/// native container. Fixed children keep their measured sizes.
pub fn flex_sizes(node: &UiNode, bounds: Size, children: &[Size]) -> Vec<Size> {
/// Allocate main-axis space with bounded growth and basis-weighted shrinking.
/// Preferred sizes come from this occurrence's first measurement in the solve;
/// remeasurement must not turn a prior allocation into a new intrinsic basis.
pub fn flex_sizes(node: &UiNode, bounds: Size, children: &[Size], preferred: &[Size]) -> Vec<Size> {
let Some((row, padding, gap)) = container(node) else {
return children.to_vec();
};
let flexible: Vec<_> = node
.children()
.iter()
.enumerate()
.filter(|(_, child)| child.flex > 0.0)
.collect();
if flexible.is_empty() {
let nodes = node.children();
if !nodes.iter().any(|child| child.has_flex()) {
return children.to_vec();
}
let fixed = children
.iter()
.zip(node.children())
.filter(|(_, child)| child.flex == 0.0)
.map(|(size, _)| if row { size.width } else { size.height })
.sum::<f32>();
let available = ((if row { bounds.width } else { bounds.height })
- padding * 2.0
- gap * children.len().saturating_sub(1) as f32
- fixed)
let space = ((if row { bounds.width } else { bounds.height }) as f64
- padding as f64 * 2.0
- gap as f64 * children.len().saturating_sub(1) as f64)
.max(0.0);
let total = flexible
let bases: Vec<f64> = nodes
.iter()
.map(|(_, child)| child.flex as f64)
.sum::<f64>();
let mut output = children.to_vec();
let mut remaining = available;
for (position, (index, child)) in flexible.iter().enumerate() {
let share = if position + 1 == flexible.len() {
remaining
} else {
((available as f64 * child.flex as f64 / total) as f32).min(remaining)
};
if row {
output[*index].width = share;
} else {
output[*index].height = share;
.zip(preferred)
.map(|(child, size)| {
child.flex_policy.basis.unwrap_or_else(|| {
if child.flex > 0.0 {
0.0
} else if row {
size.width
} else {
size.height
}
}) as f64
})
.collect();
let mut sizes: Vec<f64> = nodes
.iter()
.zip(&bases)
.map(|(child, basis)| {
basis.clamp(child.flex_policy.min as f64, child.flex_policy.max as f64)
})
.collect();
let free = space - sizes.iter().sum::<f64>();
let growing = free > 0.0;
let weights: Vec<f64> = nodes
.iter()
.zip(&bases)
.map(|(child, basis)| {
if growing {
child.flex as f64
} else {
child.flex_policy.shrink as f64 * basis
}
})
.collect();
let limits: Vec<f64> = nodes
.iter()
.map(|child| {
if growing {
child.flex_policy.max as f64
} else {
child.flex_policy.min as f64
}
})
.collect();
let mut active: Vec<usize> = (0..nodes.len())
.filter(|&i| {
weights[i] > 0.0
&& if growing {
sizes[i] < limits[i]
} else {
sizes[i] > limits[i]
}
})
.collect();
let mut remaining = free.abs();
while remaining > 0.0 && !active.is_empty() {
let total = active.iter().map(|&i| weights[i]).sum::<f64>();
let mut spent = 0.0;
let mut saturated = false;
for &i in &active {
let capacity = (limits[i] - sizes[i]).abs();
let amount = (remaining * weights[i] / total).min(capacity);
sizes[i] += if growing { amount } else { -amount };
spent += amount;
saturated |= amount >= capacity;
}
remaining = (remaining - spent).max(0.0);
// Without saturation this round distributed all available space. Do not
// spin on floating point residue; a saturated round freezes a child.
if !saturated {
break;
}
active.retain(|&i| {
if growing {
sizes[i] < limits[i]
} else {
sizes[i] > limits[i]
}
});
}
let mut output = children.to_vec();
for (size, along) in output.iter_mut().zip(sizes) {
if row {
size.width = along as f32;
} else {
size.height = along as f32;
}
remaining = (remaining - share).max(0.0);
}
output
}
@@ -508,7 +587,7 @@ impl LayoutRuntime {
continue;
}
let child_sizes: Vec<_> = item.children.iter().map(|child| sizes[*child]).collect();
let child_sizes = flex_sizes(item.node, size, &child_sizes);
let child_sizes = flex_sizes(item.node, size, &child_sizes, &child_sizes);
for (child, allocated) in item.children.iter().zip(&child_sizes) {
sizes[*child] = *allocated;
}
+16 -6
View File
@@ -148,7 +148,11 @@ impl NativeCallable for Method {
let layout = layout
.lock()
.map_err(|_| invalid("layout context poisoned"))?;
Ok::<_, VmError>((layout.node.clone(), layout.children.clone()))
Ok::<_, VmError>((
layout.node.clone(),
layout.children.clone(),
layout.preferred.clone(),
))
})
.transpose()?;
let input = object(input)?;
@@ -162,12 +166,12 @@ impl NativeCallable for Method {
x >= 0.0 && y >= 0.0 && x < geometry.width && y < geometry.height,
)));
}
if matches!(&staged, Some((None, _))) {
if matches!(&staged, Some((None, _, _))) {
return Err(invalid(
"layout context is not prepared for this occurrence",
));
}
let node = if let Some((Some(node), _)) = &staged {
let node = if let Some((Some(node), _, _)) = &staged {
node.clone()
} else {
block::snapshot(cx.store(), owner)?
@@ -181,7 +185,7 @@ impl NativeCallable for Method {
if min_w > max_w || min_h > max_h {
return Err(invalid("minimum exceeds maximum constraint"));
}
let size = if let Some((_, children)) = &staged {
let size = if let Some((_, children, _)) = &staged {
layout::measure_native(&node, children.clone(), [min_w, min_h, max_w, max_h])
.map_err(|_| invalid("native measurement exceeds layout limits"))?
} else {
@@ -205,7 +209,7 @@ impl NativeCallable for Method {
if x + width > MAX_DIMENSION as f32 || y + height > MAX_DIMENSION as f32 {
return Err(invalid("visual bounds exceed limits"));
}
let children = if let Some((_, children)) = &staged {
let children = if let Some((_, children, _)) = &staged {
children.clone()
} else {
node.children()
@@ -222,7 +226,13 @@ impl NativeCallable for Method {
})
.collect::<Result<Vec<_>, _>>()?
};
let children = layout::flex_sizes(&node, Size { width, height }, &children);
let preferred = staged
.as_ref()
.map_or(children.as_slice(), |(_, _, preferred)| {
preferred.as_slice()
});
let children =
layout::flex_sizes(&node, Size { width, height }, &children, preferred);
let points = layout::arrange_native(&node, x, y, Size { width, height }, &children)
.map_err(|_| invalid("native arrangement exceeds limits"))?;
let mut result = Vec::new();
+1
View File
@@ -118,6 +118,7 @@ mod tests {
});
let root = UiNode {
flex: 0.0,
flex_policy: crate::layout::FlexPolicy::default(),
main_align: crate::layout::MainAlign::Start,
block_id: "shared".into(),
protocol_key: "rect".into(),
+1
View File
@@ -274,6 +274,7 @@ impl RenderCache {
};
let next = UiNode {
flex: data.flex,
flex_policy: data.flex_policy,
main_align: data.main_align,
block_id: block.to_string(),
protocol_key,
+12 -4
View File
@@ -68,6 +68,7 @@ struct Plan {
nodes: Vec<Arc<UiNode>>,
children: Vec<Vec<usize>>,
sizes: Vec<Size>,
preferred: Vec<Option<Size>>,
bounds: Vec<Option<[f32; 4]>>,
runtime: LayoutRuntime,
custom: bool,
@@ -125,7 +126,7 @@ impl NativeCallable for Step {
self.phase.method(),
self.desktop,
)?;
let (node, children, bounds, native_size) = {
let (node, children, preferred, bounds, native_size) = {
let mut plan = self
.plan
.lock()
@@ -135,6 +136,10 @@ impl NativeCallable for Step {
.iter()
.map(|child| plan.sizes[*child])
.collect::<Vec<_>>();
let preferred = plan.children[self.index]
.iter()
.map(|child| plan.preferred[*child].unwrap_or(plan.sizes[*child]))
.collect::<Vec<_>>();
let bounds = if matches!(self.phase, Phase::Arrange) {
plan.bounds[self.index]
} else {
@@ -194,9 +199,9 @@ impl NativeCallable for Step {
if matches!(self.phase, Phase::Arrange) {
plan.runtime.arranged += 1;
plan.custom_arrange[self.index] =
!intrinsic || node.children().iter().any(|child| child.flex > 0.0);
!intrinsic || node.children().iter().any(|child| child.has_flex());
}
(node, children, bounds, native_size)
(node, children, preferred, bounds, native_size)
};
let context = mount.layout.ok_or(invalid("missing pre-layout context"))?;
{
@@ -205,6 +210,7 @@ impl NativeCallable for Step {
.map_err(|_| invalid("layout context poisoned"))?;
context.node = Some(node);
context.children = children;
context.preferred = preferred;
context.bounds = bounds;
}
if let Some(size) = native_size {
@@ -385,6 +391,7 @@ impl NativeCallable for Collect {
);
}
}
plan.preferred[self.index].get_or_insert(size);
plan.sizes[self.index] = size;
if self.index == 0 {
plan.bounds[0] = Some([0.0, 0.0, size.width, size.height]);
@@ -686,7 +693,7 @@ pub fn render(
let mut nodes = Vec::new();
let mut work = vec![root.clone()];
while let Some(node) = work.pop() {
custom |= node.flex > 0.0;
custom |= node.has_flex();
work.extend(node.children().iter().rev().cloned());
nodes.push(node);
}
@@ -727,6 +734,7 @@ pub fn render(
let count = nodes.len();
let plan = Arc::new(Mutex::new(Plan {
sizes: vec![Size::default(); nodes.len()],
preferred: vec![None; nodes.len()],
bounds: vec![None; nodes.len()],
nodes,
children,
+7 -1
View File
@@ -61,6 +61,7 @@ pub const QUERY_NAMES: &[&str] = &[
#[derive(Debug, Clone, PartialEq)]
pub struct UiNode {
pub flex: f32,
pub flex_policy: crate::layout::FlexPolicy,
pub main_align: crate::layout::MainAlign,
pub block_id: String,
pub protocol_key: String,
@@ -80,8 +81,13 @@ impl UiNode {
}
}
pub fn has_flex(&self) -> bool {
self.flex > 0.0 || self.flex_policy != crate::layout::FlexPolicy::default()
}
pub fn shallow_eq(&self, other: &Self) -> bool {
if self.flex != other.flex
if self.flex_policy != other.flex_policy
|| self.flex != other.flex
|| self.main_align != other.main_align
|| self.block_id != other.block_id
|| self.protocol_key != other.protocol_key