DioxusLabs / taffy

A high performance rust-powered UI layout library
https://docs.rs/taffy
Other
2.04k stars 102 forks source link

Negative margins in flexbox result in incorrect layout #706

Open jamesthurley opened 3 weeks ago

jamesthurley commented 3 weeks ago

taffy version

0.5.2

Platform

Rust

What you did

I'm trying to use negative margins to bleed child elements into the parent padding, however the resulting layout appears to be incorrect.

The simplest example I've come up with is the following code:

use taffy::prelude::*;

fn main() {
    let mut tree: TaffyTree<NodeContext> = TaffyTree::new();

    let padding = 5.;
    let negative_padding = -1. * padding;

    let child_1_style = Style {
        flex_basis: auto(),
        flex_grow: 0.,
        flex_shrink: 0.,
        // margin: Rect {
        //     top: length(negative_padding),
        //     left: length(negative_padding),
        //     right: length(negative_padding),
        //     bottom: length(0.),
        // },
        ..Default::default()
    };

    let child_1_node = tree
        .new_leaf_with_context(child_1_style, NodeContext::Spacer)
        .unwrap();

    let child_2_style = Style {
        flex_basis: auto(),
        flex_grow: 1.,
        flex_shrink: 1.,
        ..Default::default()
    };

    let child_2_node = tree
        .new_leaf_with_context(child_2_style, NodeContext::Spacer)
        .unwrap();

    let root_style = Style {
        size: Size {
            width: Dimension::Percent(1.),
            height: Dimension::Percent(1.),
        },
        display: Display::Flex,
        flex_direction: FlexDirection::Column,
        justify_content: Some(JustifyContent::FlexStart),
        padding: Rect {
            top: length(padding),
            left: length(padding),
            right: length(padding),
            bottom: length(padding),
        },
        ..Default::default()
    };

    let root_node = tree
        .new_with_children(root_style, &[child_1_node, child_2_node])
        .unwrap();

    tree.compute_layout_with_measure(
        root_node,
        Size {
            width: length(1000.),
            height: length(800.),
        },
        |known_dimensions, available_space, _node_id, node_context, _style| {
            measure_function(known_dimensions, available_space, node_context)
        },
    )
    .unwrap();

    tree.print_tree(root_node);
}

enum NodeContext {
    Spacer,
}

fn measure_function(
    known_dimensions: taffy::geometry::Size<Option<f32>>,
    _available_space: taffy::geometry::Size<taffy::style::AvailableSpace>,
    node_context: Option<&mut NodeContext>,
) -> Size<f32> {
    if let Size {
        width: Some(width),
        height: Some(height),
    } = known_dimensions
    {
        return Size { width, height };
    }

    match node_context {
        None => Size::ZERO,
        Some(NodeContext::Spacer) => Size {
            width: 10.,
            height: 10.,
        },
    }
}

Note that root_node has padding of 5px, child_1_node should be as small vertically as necessary, child_2_node should take the remaining space. I'm computing the layout for a size of 1000 x 800.

The above code has the negative margins on child_1_style commented out, and the output is as expected:

TREE
└──  FLEX COL [x: 0    y: 0    w: 1000 h: 800  content_w: 995  content_h: 795  border: l:0 r:0 t:0 b:0, padding: l:5 r:5 t:5 b:5] (NodeId(4294967299))
    ├──  LEAF [x: 5    y: 5    w: 990  h: 10   content_w: 10   content_h: 10   border: l:0 r:0 t:0 b:0, padding: l:0 r:0 t:0 b:0] (NodeId(4294967297))
    └──  LEAF [x: 5    y: 15   w: 990  h: 780  content_w: 10   content_h: 10   border: l:0 r:0 t:0 b:0, padding: l:0 r:0 t:0 b:0] (NodeId(4294967298))

child_1 starts at y: 5 due to the parent's top padding, and has a height of 10. child_2 starts at y: 15 and has height of 780, leaving 5 for the parent's bottom padding.

This roughly corresponds to this layout (ignore overall size, it's a rough HTML equivalent): image

However, if I uncomment the negative margins on child_1, I would expect the following layout:

image

But instead I get this:

TREE
└──  FLEX COL [x: 0    y: 0    w: 1000 h: 800  content_w: 1000 content_h: 800  border: l:0 r:0 t:0 b:0, padding: l:5 r:5 t:5 b:5] (NodeId(4294967299))
    ├──  LEAF [x: 0    y: 0    w: 1000 h: 10   content_w: 10   content_h: 10   border: l:0 r:0 t:0 b:0, padding: l:0 r:0 t:0 b:0] (NodeId(4294967297))
    └──  LEAF [x: 5    y: 10   w: 990  h: 790  content_w: 10   content_h: 10   border: l:0 r:0 t:0 b:0, padding: l:0 r:0 t:0 b:0] (NodeId(4294967298))

child_1 is correctly starting at y: 0, bleeding into the parent padding as expected, with a height of 10. child_2 correctly starts at y: 10, but the height is 790, leaving no space for the parent's padding at the bottom. I would expect it to have a height of 785.

Note child_2 does not have any negative margins, and should not be bleeding into the parent's padding. It seems like it's correctly been given extra space due to child_1 moving up, but has used twice as much extra space as it should have.

If you add an extra child which does bleed into the bottom margin:


    let child_3_style = Style {
        flex_basis: auto(),
        flex_grow: 0.,
        flex_shrink: 0.,
        margin: Rect {
            top: length(0.),
            left: length(negative_padding),
            right: length(negative_padding),
            bottom: length(negative_padding),
        },
        ..Default::default()
    };

    let child_3_node = tree
        .new_leaf_with_context(child_3_style, NodeContext::Spacer)
        .unwrap();

You get the following:

TREE
└──  FLEX COL [x: 0    y: 0    w: 1000 h: 800  content_w: 1000 content_h: 810  border: l:0 r:0 t:0 b:0, padding: l:5 r:5 t:5 b:5] (NodeId(4294967300))
    ├──  LEAF [x: 0    y: 0    w: 1000 h: 10   content_w: 10   content_h: 10   border: l:0 r:0 t:0 b:0, padding: l:0 r:0 t:0 b:0] (NodeId(4294967297))
    ├──  LEAF [x: 5    y: 10   w: 990  h: 790  content_w: 10   content_h: 10   border: l:0 r:0 t:0 b:0, padding: l:0 r:0 t:0 b:0] (NodeId(4294967298))
    └──  LEAF [x: 0    y: 800  w: 1000 h: 10   content_w: 10   content_h: 10   border: l:0 r:0 t:0 b:0, padding: l:0 r:0 t:0 b:0] (NodeId(4294967299))

child_3 is rendered completely outside the box (y: 800), when it should look like this:

image

What went wrong

Negative margins seem to result in the layout being computed incorrectly.

Related to https://github.com/DioxusLabs/taffy/issues/475