Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

There should be a way to position bevy_ui children independently of parent Flex parameters #9167

Open
nicopap opened this issue Jul 15, 2023 · 10 comments
Labels
A-UI Graphical user interfaces, styles, layouts, and widgets C-Feature A new feature, making something new possible

Comments

@nicopap
Copy link
Contributor

nicopap commented Jul 15, 2023

Bevy version

main: ede5848

What you did

I set the border of a node with a PositionType::Absolute child.

What went wrong

The child's position changed according to the border width, while it shouldn't, as Absolute implies.

Additional information

Click to see Minimal reproducible example
use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, setup)
        .add_systems(Update, update_border)
        .run();
}
#[derive(Component)]
struct UpdateBorder;
fn update_border(
    mut q: Query<&mut Style, With<UpdateBorder>>,
    input: Res<Input<KeyCode>>,
    mut expanded: Local<bool>,
) {
    if !input.just_pressed(KeyCode::Space) { return; }
    if let Ok(mut style) = q.get_single_mut() {
        let border = if *expanded { 0.0 } else { 10.0 };
        style.border = UiRect::all(Val::Px(border));
        *expanded = !*expanded;
    }
}
fn setup(mut cmds: Commands) {
    cmds.spawn(Camera2dBundle::default());
    let style = TextStyle { color: Color::BLACK, ..default() };
    cmds
        .spawn((
            NodeBundle {
                border_color: Color::RED.into(),
                background_color: Color::YELLOW.into(),
                style: Style { width: Val::Px(300.0), height: Val::Px(50.0), ..default() },
                ..default()
            },
            UpdateBorder,
        ))
        .with_children(|cmds| {
            cmds.spawn(TextBundle {
                style: Style {
                    position_type: PositionType::Absolute,
                    left: Val::Px(0.),
                    right: Val::Px(0.),
                    top: Val::Px(0.),
                    bottom: Val::Px(0.),
                    ..default()
                },
                ..TextBundle::from_section("Bad border!", style)
            });
        });
}

border_before2023-07-15

border_after2023-07-15

@nicopap nicopap added C-Bug An unexpected or incorrect behavior A-UI Graphical user interfaces, styles, layouts, and widgets labels Jul 15, 2023
@Ziplodocus
Copy link

This is the same behaviour as in CSS positioning, I'd leave it as is for consistency personally.
Honestly not sure what is more intuitive and I don't have strong feelings either way.
Seems to me that changing the behaviour here would be more trouble than it would be worth.
Could just use negative value for the positioning here if that's the desired behaviour 🤷

@nicopap
Copy link
Contributor Author

nicopap commented Jul 15, 2023

This is the same behaviour as in CSS positioning

What do you mean? If an element is set as position: absolute and you set fields left: 0 and top: 0, the element will be stuck at the top left of the screen, ignoring the parent's position/offset/layout.

@nicopap
Copy link
Contributor Author

nicopap commented Jul 15, 2023

Ok, maybe you meant the behavior with regard to the "containing block". Though I'm not sure I follow what the mdn dock is about here1.

You seem to know quite well CSS, could you link to documentation in question? It's very easy to have a wrong idea of what's going on. I personally tried a few possible combination of properties in the mdn playground before opening this issue, and still have little idea of the correct behavior.

In any case bevy already doesn't follow web standards with the Absolute position type, since it will always be positioned wrt to the parent. So maybe it's worthwhile to not repeat exactly as-is the mistakes of CSS. Instead making Absolute useful?

I think, ideally, UI rendering should be fully decoupled from UI layouting, allowing for 3rd party layout algos to benefit from the UI rendering without having to deal with the very thing they try to replace.

A potential alternative is to add a "do not compute" PositionType which would also allow reducing overhead of layout computation, as well as enabling this specific use-case.

Your proposed solution is extremely cumbersome to implement for my usecase, check the linked issue for an explanation why it would be impractical.

Lastly, I thought of a different workaround, which is to set the Transform directly after the UiSystem::Layout system.

Footnotes

  1. (from https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block) "If the position property is absolute, the containing block is formed by the edge of the padding box of the nearest ancestor element that has a position value other than static"2

  2. This seems to indicate I should be able to set padding to -border_width to counter the offset. But padding does nothing to PositionType::Absolute.

@TimJentzsch
Copy link
Contributor

This is the intended behavior and works like in CSS:

The same example in HTML/CSS
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <style>
      html,
      body {
        margin: 0;
      }

      div {
        display: flex;
        box-sizing: border-box;
        position: relative;
      }
    </style>
  </head>
  <body>
    <div
      style="
        background-color: yellow;
        border: solid red 10px;
        width: 300px;
        height: 50px;
      "
    >
      <div style="position: absolute; left: 0; right: 0; top: 0; bottom: 0">
        Bad border!
      </div>
    </div>
  </body>
</html>

The same example in HTML/CSS. The text is also placed within the border of the containing element and doesn't overlap it.

This is because, even with absolute position, the child is still positioned within the content area of the parent element, which does not include the border: https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block

Solution

I know of two ways to solve this problem in HTML/CSS, from my testing it seems like they both work in Bevy as well:

Negative Margin of Child

The first option is to give the child element a negative margin equal to the border width.
This works well if you need other, relatively positioned child elements that do not overlap with the border.
However, it will be more annoying to update the border on interaction, as you need to query for the child element as well to update the margin.

HTML/CSS code sample
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <style>
      html,
      body {
        margin: 0;
      }

      div {
        display: flex;
        box-sizing: border-box;
        position: relative;
      }
    </style>
  </head>
  <body>
    <div
      style="
        background-color: yellow;
        border: solid red 10px;
        width: 300px;
        height: 50px;
      "
    >
      <div
        style="
          position: absolute;
          margin: -10px;
          left: 0;
          right: 0;
          top: 0;
          bottom: 0;
        "
      >
        Bad border!
      </div>
    </div>
  </body>
</html>

Screenshot of the behavior with the first code sample. The text overlaps with the border.

Border of Child Element

The second option is to add the border via another child element, instead of adding it directly to the parent.
The child with the text is not part of the regular layout flow, so it will overlap the child adding the border.
This approach would make it easier to update the border on interaction, because you don't have to query for the child element as well. But it doesn't work well if you want other child elements in the parent, because they will also be placed outside of the border.

HTML/CSS code sample
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <style>
      html,
      body {
        margin: 0;
      }

      div {
        display: flex;
        box-sizing: border-box;
        position: relative;
      }
    </style>
  </head>
  <body>
    <div style="background-color: yellow; width: 300px; height: 50px">
      <div
        style="
          position: absolute;
          border: solid red 10px;
          width: 100%;
          height: 100%;
        "
      ></div>
      <div style="position: absolute; left: 0; right: 0; top: 0; bottom: 0">
        Bad border!
      </div>
    </div>
  </body>
</html>

Screenshot of the behavior with the second code sample. The text overlaps with the border.

@nicopap
Copy link
Contributor Author

nicopap commented Jul 16, 2023

Any solution that requires propagating the offset to the children is a no-go for my use case. Ideally, I could just turn off layout computation.

@Ziplodocus
Copy link

Ziplodocus commented Jul 18, 2023

In any case bevy already doesn't follow web standards with the Absolute position type, since it will always be positioned wrt to the parent.

It seems the issue here is that the only two position types are Relative and Absolute so as you say above, in Bevy a node is always going to to be positioned relative to the parent, I think.

I suppose either a PositionType::Static or a PositionType::Fixed might solve your issue? I'd be happy to give a go at implementing one of these, or at least see how far I could get, unless there's a reason they don't exist already?

@nicopap nicopap added C-Feature A new feature, making something new possible and removed C-Bug An unexpected or incorrect behavior labels Jul 18, 2023
@nicopap nicopap changed the title bevy_ui: border_width influences positioning of PositionType::Absolute children There should be a way to position bevy_ui children independently of parent Flex parameters Jul 18, 2023
@nicopap
Copy link
Contributor Author

nicopap commented Jul 18, 2023

I'd love to see that! It would be great. It was probably overlooked because "why the hell would you need that?" Which is fair ahah. until someone (like me) has a usecase you don't expect.

@nicoburns
Copy link
Contributor

A couple of notes:

  • While the border property in CSS is inherently layout-affecting, CSS also has the outline property which produces a visual border but without affecting layout. By default this is painted outside of the node that has the "outline" (hence the name), but this can be controlled by the outline-offset property. The CSS outline: 10px; outline-offset: -10px on the parent node would achieve the affect you are after.

  • Ideally, UI rendering should be fully decoupled from UI layouting, allowing for 3rd party layout algos to benefit from the UI rendering without having to deal with the very thing they try to replace.

    My ideal here would be for layout to be customisable (including use of 3rd-party layout algorithms) on a node-by-node basis. So could use CSS layouting for one node, and something like cuicui layout (or morphorm, or a user-specified custom layout) for another. In a future world where we have higher-level widgets, it would also mean that different widgets could use different layout algorithms if they wanted to while still being interoperable. Taffy already has a trait that is aimed at facilitating this kind of interop between layout algorithms (https://github.com/DioxusLabs/taffy/blob/main/src/tree/mod.rs). Although it is only really usable in the (unreleased) 0.4 version.

@nicoburns
Copy link
Contributor

It seems the issue here is that the only two position types are Relative and Absolute so as you say above, in Bevy a node is always going to to be positioned relative to the parent, I think.
I suppose either a PositionType::Static or a PositionType::Fixed might solve your issue? I'd be happy to give a go at implementing one of these, or at least see how far I could get, unless there's a reason they don't exist already?
...
I'd love to see that! It would be great. It was probably overlooked because "why the hell would you need that?" Which is fair ahah. until someone (like me) has a usecase you don't expect.

The lack of position: static and position: fixed is indeed why nodes are always positioned relative to their parent. The reason they haven't been implemented isn't because they aren't considered useful, but because it's non-trivial to work out what the API should be to expose this feature. You effectively end up with the output nodes (sometimes) being in different places in the tree to input nodes, but Taffy can currently only represent a single hierarchy.

There are also complications for people wanting to embed Taffy in a widget system that is not "all in" on Taffy and that doesn't give the layout algorithm access to the entire tree at once (although we could likely just not support this feature for that use case while still supporting it in the case that you do have access to the entire tree)

@nicoburns
Copy link
Contributor

#9931 implements the outline property mentioned above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-UI Graphical user interfaces, styles, layouts, and widgets C-Feature A new feature, making something new possible
Projects
None yet
Development

No branches or pull requests

4 participants