Skip to content

Conversation

alice-i-cecile
Copy link
Member

@alice-i-cecile alice-i-cecile commented Jul 31, 2022

Note for release notes: this PR is co-authored with @Weibye <3

Objective

Changelog

  • broke the Style UI component into many small components
  • added the FlexboxLayoutQuery, FlexboxLayoutQueryItem and FlexboxLayoutChanged helper types to make recomputing layouts easier

Migration Guide

Up to date as of 76557be

The Style-component has been removed. In its place, there are several new components:

  • LayoutControl
  • FlexContainer
  • FlexItem
  • SizeConstraints
  • Spacing
Previous location New location
Style.direction: Direction ❗ Removed. Bevy does not currently support multiple text-directions ❗
LayoutControl
Style.position_type: PositionType LayoutControl.position: Position
Style.position: UiRect LayoutControl.inset: Inset(UiRect)
Style.overflow: Overflow LayoutControl.overflow: Overflow
Style.display: Display LayoutControl.display: Display
FlexContainer
Style.flex_direction: FlexDirection FlexContainer.direction: Direction
Style.flex_wrap: FlexWrap FlexContainer.wrap: Wrap
Style.align_items: AlignItems FlexContainer.align_items: AlignItems
Style.align_content: AlignContent FlexContainer.align_content: AlignContent
Style.justify_content: JustifyContent FlexContainer.justify_content: JustifyContent
Style.gap: Gap(UiRect) FlexContainer.gap: Gap(UiRect)
FlexItem
Style.align_self: AlignSelf FlexItem.align_self: AlignSelf
Style.flex_grow: f32 FlexItem.flex_grow: f32
Style.flex_shrink: f32 FlexItem.flex_shrink: f32
Style.flex_basis: Val FlexItem.flex_basis: Val
Spacing
Style.margin: UiRect Spacing.margin: UiRect
Style.padding: UiRect Spacing.padding: UiRect
Style.border: UiRect Spacing.border: UiRect
SizeConstraints
Style.size: Size SizeConstraints.suggested: Size
Style.min_size: Size SizeConstraints.min: Size
Style.max_size: Size SizeConstraints.max: Size
Style.aspect_ratio: Option<f32> SizeConstraints.aspect_ratio: Option<f32>

Queries

Any queries that previously queried all of Style should now instead use the FlexLayoutQuery.

// old
fn my_system(q: Query<&Style>) { .. }
// new
fn my_system(q: Query<FlexlayoutQuery>) { .. }

Most of the time however, you'd likely want to query for the more specific layout components you need.

Change detection

If you previously relied on detecting when something on Style changed you can now instead use the FlexLayoutChanged type.

// old
fn my_system(q: Query<.., Changed<Style>>) { .. }
// new
fn my_system(q: Query<.., Changed<FlexLayoutChanged>>) { .. }

Small related changes

Various helper methods on TextBundle have been removed for consistency with other bundles in the engine. Construct these explicitly, or use a trait extension method to redefine them if you prefer this style.

@Weibye Weibye added A-UI Graphical user interfaces, styles, layouts, and widgets C-Code-Quality A section of code that is hard to understand or change M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide labels Jul 31, 2022
@alice-i-cecile alice-i-cecile marked this pull request as draft July 31, 2022 15:58
@alice-i-cecile alice-i-cecile added the C-Usability A targeted quality-of-life change that makes Bevy easier to use label Jul 31, 2022
@alice-i-cecile alice-i-cecile changed the title Style fracture Refactor Style component into many smaller pieces Jul 31, 2022
Copy link
Contributor

@Weibye Weibye left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good in my eyes, I particularly like the changed filter!

I do share your concern about example-migrating. That being said, are there one relatively simple example we could migrate while still in draft-phase so we can better see the actual implication on user-code?

@IceSentry
Copy link
Contributor

I'd suggest only migrating the ui example for now. It's complex enough to show the use of this new api without being too complex.

Copy link
Contributor

@IceSentry IceSentry left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how I feel about the general concept yet. I need to see it in action first, but I'm a bit concerned that it will reduce discoverability of all the style parameters. Fortunately we could have a builder type thing so it might be fine.

I like that it flattens styling a bit and puts color at the same level as everything else. I also like the rename to Layout.

My main criticism right now is that all instances of Flexbox should be renamed to Flex

@sixfold-origami sixfold-origami self-requested a review July 31, 2022 17:04
@IceSentry
Copy link
Contributor

IceSentry commented Jul 31, 2022

This should probably be done in a future PR just to not let this one grow too much, but doing this for TextStyle should probably be done too.

@Nilirad
Copy link
Contributor

Nilirad commented Aug 1, 2022

I'm a bit concerned that it will reduce discoverability of all the style parameters.

We can group the components under a module, so they are listed in rendered documentation.

@Weibye
Copy link
Contributor

Weibye commented Aug 21, 2022

Been thinking a bit about this recently, so here's some deeper reasoning and context:

Let's consider the use case of us implementing borders: (#3991)

  • We want to support creating borders on rects.
  • This should support border width, border radius, color / material and a few other properties.
  • Rendering of the border and layouting of the border should be the same. I.e there should only be one source of truth for setting these properties.

Option 1

  • To set border width, user sets the border property in Style
  • To set border color, border radius, the user sets the properties on some completely different component BorderComponent

This has the downside of splitting concerns across multiple components without providing consistency.

Option 2

  • All border-related properties are moved into BorderComponent and out of Style
  • Adding a border to a node is now simple as only a single component is concerned.
  • During layouting, we query the BorderComponent.rect to feed the data into the layout calculation

The end-user experience is better in this situation, but it does fragment Style.

Option 3

  • All Style-properties are fragmented fully.
  • BorderWidthComponent, BorderColorComponent, BorderRadiusComponent are introduced.
  • In order to setup borders on nodes, some or all of these components must be present

End user experience is much more verbose, but this is technically closer to how CSS actually operates.


I think option 2 or 3 is the best way to go as they provide much more consistency, and they pave the way to unlock more style functionality that isn't related to layouting.

Option 3 is probably the best if, and only if we manage to build higher level ergonomics on top, but that is something that has to come later. We can start with path 2, then go to 3 later.

My conclusion is that we need this merged to unlock the way forward.

@StarArawn
Copy link
Contributor

How does one handle reusable styles when its fragmented into multiple components like this? Perhaps a separate monolithic style bundle? 🤔Internally I'm still debating if styles should be attached to entities or if they should be treated more like materials that live inside of the asset system.

@Weibye
Copy link
Contributor

Weibye commented Aug 27, 2022

How does one handle reusable styles when its fragmented into multiple components like this? Perhaps a separate monolithic style bundle? 🤔Internally I'm still debating if styles should be attached to entities or if they should be treated more like materials that live inside of the asset system.

Here's how I'm picturing it at a high level:

  1. The style-components are completely fragmented. Their job is only to tell layouting and rending of what these nodes should look like right now. They just store the runtime data.
  2. We have some asset that defines a theme. Likely using scenes, but behaves like css. This asset stores targets and the styling data to be applied to them.
  3. A theming-plugin / system is responsible for getting the theme-asset as input, queries for the nodes specified, then inject new data to the various style-components.
  4. Rendering and layouting is run, in which they simply query for the style-components they need to look at.

The heavy lifting will be done by the theming-plugin, and we should try to aim for a design that separates theme from style.

@TimJentzsch
Copy link
Contributor

I would suggest to reorder the migration table a bit to have all flexbox layout stuff together.

@alice-i-cecile
Copy link
Member Author

alice-i-cecile commented Aug 27, 2022

How does one handle reusable styles when its fragmented into multiple components like this? Perhaps a separate monolithic style bundle?

The "store each style as an entity" model in bevyengine/rfcs#1 would work well with more scattered styling components. It's also fully extensible to user-defined style properties.

We already have issues with this; other things you might want to share (such as color) live outside of the Style component.

Copy link
Contributor

@TimJentzsch TimJentzsch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My initial impression is very positive, this seems to be much more natural for ECS. It also facilitates reuse across different application areas and avoids having "unneded" style definitions for entities that only need a part of them.

Where exactly to make the split is probably the hard part, for example it's a bit weird from a user perspective to define the border size and the border color in separate places. But we already partially had this problem before.

I also assume that not every layout system will need all of these components, so maybe it's even a bit more efficient.

For now, I left some doc comments

Copy link
Contributor

@Weibye Weibye left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have:

  1. Brought this up to speed on main
  2. Migrated examples
  3. LayoutStrategy -> Layout

Some of these new structs lend itself very well to various builder patterns like Spacing while others are not so clear how to best expand and improve, but we don't have to solve all of that now we can continually improve as we go :)

Comment on lines +275 to +279
/// Creates a [`UiRect`] where all sides are [`Val::Undefined`]
pub const UNDEFINED: UiRect = UiRect::all(Val::Undefined);

/// Creates a [`UiRect`] where all sides are [`Val::Auto`]
pub const AUTO: UiRect = UiRect::all(Val::Auto);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have it documented somewhere what the difference between undefined and auto is?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TimJentzsch In general the meaning of Auto depends on the property being set. For a Rect, the main property where Auto is relevant is margin where Auto means "take up remaining available space" and Undefined resolves to zero. Currently Taffy and (bevy_ui) also accept Auto for padding and border where it is no different to Undefined. Taffy 0.3 (unreleased) is more strict about this and introduces a new type with no Auto variant for padding and border. I'm unsure as whether bevy_ui will follow Taffy in this regard.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unsure as whether bevy_ui will follow Taffy in this regard.

I would be strongly in favour of doing so.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At some point we should probably have that in the doc comments as well, of the respective components.


/// The inset of a UI node from its base position.
///
/// Layouting is performed by the Flexbox layout algorithm where the value of [`Inset`] is considered.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is it considered?
IMO this needs a bit more explanation what this actually does, especially because this is not a property in CSS (to my knowledge)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inset is what we until now has (incorrectly) been calling position. It works the same way, it just has been renamed.
https://developer.mozilla.org/en-US/docs/Web/CSS/inset

See DioxusLabs/taffy#271

Better docs never hurt though, so will look into that

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a property in CSS. It's the shorthand name for the top, left, right, and bottom properties. Taffy previously called this position (and what is now position was called position_type). They have been renamed to inset and position to match CSS. https://developer.mozilla.org/en-US/docs/Web/CSS/inset

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the doc-comment a bit, hopefully to be a bit clearer.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, looks like my search was off.
I like that follow CSS terminology!

@ickshonpe
Copy link
Contributor

ickshonpe commented Jan 18, 2023

The way the constraints data is structured seems wrong to me.
In Flex you usually access the constraints one axis at a time but the current API makes that awkward and fragile.
For example, look at the text_constraint function in widget/text.rs:

pub fn text_constraint(min_size: Val, size: Val, max_size: Val, scale_factor: f64) -> f32 {
    match (min_size, size, max_size) {
       (_, _, Val::Px(max)) => scale_value(max, scale_factor),
       // .. 
    }
}

called with:

text_constraint(
      layout.size_constraints.min.width,
      layout.size_constraints.suggested.width,
      layout.size_constraints.max.width,
      scale_factor,
 )

With the constraints data stored per-axis:

pub struct LengthConstraint {
    pub min: Val,
    pub max: Val,
    pub suggested: Val,
} 

it's much more natural:

pub fn text_constraint(length_constraint: LengthConstraint, scale_factor: f64) -> f32 {
    match length_constraint {
        LengthConstraint { max: Val::Px(max), .. } => scale_value(max, scale_factor),
        // .. 
    }
}
text_constraint(layout.size_constraints.width, scale_factor)

In Bevy that is the worst it gets at the moment, but in Taffy there are lots of tangled sections like:

 if constants.node_inner_size.main(constants.dir).is_none() && constants.is_row {
            child.target_size.set_main(
                constants.dir,
                child.size.main(constants.dir).unwrap_or(0.0).maybe_clamp(
                    child.resolved_minimum_size.main(constants.dir).into(),
                    child.max_size.main(constants.dir),
                ),
            );
        } else {
            child.target_size.set_main(constants.dir, child.hypothetical_inner_size.main(constants.dir));
        }

where most of the complexity would vanish with some sort of per-axis interface.

@alice-i-cecile
Copy link
Member Author

I agree with that. @Weibye, do you think we should implement that now, or fix it upstream first?

@Weibye
Copy link
Contributor

Weibye commented Jan 19, 2023

I agree with that. @Weibye, do you think we should implement that now, or fix it upstream first?

If possible I'd prefer getting this merged in its current state, then tackle that in a separate PR so that discussion can take place more focused, and we can get some movement on these refactors.

I do think we should fix the axis separation upstream first, then fix it here after.

@nicoburns
Copy link
Contributor

I do think we should fix the axis separation upstream first, then fix it here after.

I'm not even completely convinced that this would an improvement for Taffy. It's true that accessing lots of values in a single axis is awkward atm. But equally it makes accessing both components of the same value easy, and that does happen a lot too. For example, applying aspect ratio is a single method call on Size<T> atm and would be pretty awkward to do if the two components were separated.

Comment on lines +28 to +32
pub control: LayoutControl,
/// Defines how this node's layout should be.
pub layout: FlexContainer,
/// Defines how this node should behave as a child of a node.
pub child_layout: FlexItem,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a general pattern we call Bundle fields the same thing as their component. Calling them something different creates a conceptual disconnect. Ex: layout: FlexContainer should be flex_container: FlexContainer. Creating a new concept / name just adds one more term for people to contend with.

/// The strategy used to position this node.
pub position: Position,
/// The inset of this UI node, relative to its default position
pub inset: Inset,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the extra type wrapper here? It doesn't add any functionality and just makes it harder to set and access these fields.

/// Core controls for layouting of this node.
///
/// See: [`Display`](crate::Display), [`Position`](crate::Position), [`Inset`](crate::Inset), [`Overflow`](crate::Overflow).
pub control: LayoutControl,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

control feels like a pretty ambiguous term in this context, given that it is a verb. What does it control? What if we later have some other XControl thing? It also feels like the wrong thing to focus on given that LayoutControl is about "layout", not "control".

/// Grouping of core control parameter of the layout for this node.
#[derive(Component, Copy, Clone, PartialEq, Debug, Serialize, Deserialize, Reflect)]
#[reflect(PartialEq, Serialize, Deserialize)]
pub struct LayoutControl {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is the "universal layout configuration component", maybe we can shorten the name to Layout? Like Display is really the "layout method selector", but we're keeping it aligned with CSS terminology, so that frees up the Layout term. This component encapsulates the "canonical layout method selector", so Layout feels like fair game.

color: Color::BLACK,
};

commands
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example is broken for me in this PR:
image

.spawn((
NodeBundle {
style: Style {
size_constraints: SizeConstraints::FILL_PARENT,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example is broken for me in this PR:
image

/// Grouping of core control parameter of the layout for this node.
#[derive(Component, Copy, Clone, PartialEq, Debug, Serialize, Deserialize, Reflect)]
#[reflect(PartialEq, Serialize, Deserialize)]
pub struct LayoutControl {
Copy link
Member

@cart cart Jan 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think theres a strong argument to put all layout related things under a single Layout component (aka dont break things up, just rename Style to Layout). Outside of the more granular change detection (which doesn't matter for the actual Taffy layout impl and will likely rarely come up in user code), I don't think breaking this up meaningfully improves UX. And it actively degrades it in a number of places:

  • Introduces more "virtual" concept nesting users need to contend with: Spacing (not a css term), Flex Container/Item (a css term, but not one users directly interact with in flex apis), SizeConstraints (not a CSS term, size_constraints.min diverges from min_size), inset (a css term, but not one commonly used, and never as layout.inset.top)
  • Discoverability is more challenging: users setting layouts now need to know how to access 5 components instead of 1. Additionally, having one component means you can discover all fields via autocomplete.
  • Ergonomics downgrade: a user that wants to switch a node to "flex", set the flex container fields, and set the flex item fields now needs to write Query<(&mut LayoutControl, &mut FlexContainer, &mut FlexItem)> . That is a pretty big mouthful compared to Query<&mut Layout>.
    // centralized
    fn system(query: Query<&mut Layout>) {
      let mut layout = query.single();
      layout.display = Display::Flex;
      layout.flex_direction = Direction::Row;
      layout.grow = 0.5;
    }
    
    // broken up
    fn system(query: Query<(&mut LayoutControl, &mut FlexContainer, &mut FlexItem)>) {
      let (mut layout_control, mut flex_container, mut flex_item) = query.single();
      layout_control.display = Display::Flex;
      flex_container.direction = Direction::Row;
      flex_item.grow = 0.5;
    }
  • Ergonomics downgrade: more nesting and verticality during instantiation:
    // centralized
    commands.spawn(NodeBundle {
        position: Position::Absolute,
        flex_direction: Direction::Row,
        grow: 0.5,
        ..default()
    })
    
    // broken up 
    commands.spawn(NodeBundle {
        control: LayoutControl {
            position: Position::Absolute,
            ..default()
        },
        flex_container: FlexContainer {
            direction: Direction::Row,
            ..default()
        },
        flex_item: FlexItem {
            grow: 0.5,
            ..default()
        },
        ..default()
    })

In theory, breaking things up into components could make things more modular. But in practice, Taffy is monolithic and a good portion of these concepts are deeply interrelated. And the centralized Display enum and "black box" nature of taffy from a user perspective prevents people from inserting new layout approaches into this meaningfully.

If I ask myself "will users be happy about these changes", in the current state I think the answer is likely "no". Maybe I'm wrong, but I'm not seeing the wins here.

Copy link
Contributor

@nicoburns nicoburns Jan 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what others' motivations for this change are (I rather agree that it's not great from an ergonomics point of view, but one key benefit would be lower memory usage in the common case that not all styles are set. Taffy's Style struct is currently around 250 bytes. It's up to 300+ with CSS Grid support in Taffy 0.3, and is only likely to increase in future when add support for things like Morphorm layout and RTL text/flow-relative styles. Granted, we're probably only talking about 5-10mb here for realistic UI node counts of ~10000 (I'm not sure how big a deal that would be considered).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we going to support other layout algorithms in Bevy, e.g. grids?
In that case I think it might make sense to keep them separated

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's my ultimate hope.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit late to the party here:

In theory, breaking things up into components could make things more modular.

The original intent behind this was to break everything up so that things like Margin, Padding, Display, Direction would be top level components in order to get close to the "behaviour" of CSS.

Then over time it morphed to what you see here which is due to multiple good arguments & various pushbacks.

I do agree with closing this particular PR, but I keep having the feeling that the original idea would be worthwhile still but it will require a different execution.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to me that the intent of the proposal is to make NodeBundle into a flat namespace. But currently that would entail each style being it's own bevy_ecs Component, which is considered undesirable for perf reasons. Perhaps a possble alternative would be some way of mapping bundles to components that didn't just assume that each property of the bundle struct is it's own component, but allows a custom mapping.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to me that the intent of the proposal is to make NodeBundle into a flat namespace. But currently that would entail each style being it's own bevy_ecs Component, which is considered undesirable for perf reasons.

Yeah, you sum it up very well here. The idea was to achieve increased ergonomics and more granular change detection but I'm starting to get the feeling that this is trying to solve the symptom instead of the root cause.

Ergonomic UI in Bevy is IMO more likely to be achieved with finding a better way to structure data-flow and high level components instead of just changing the api surface of what is already there. In short: We need a working design :)

@mockersf mockersf removed this from the 0.10 milestone Jan 24, 2023
@ickshonpe
Copy link
Contributor

ickshonpe commented Jan 30, 2023

I'm not even completely convinced that this would an improvement for Taffy. It's true that accessing lots of values in a single axis is awkward atm. But equally it makes accessing both components of the same value easy, and that does happen a lot too. For example, applying aspect ratio is a single method call on Size<T> atm and would be pretty awkward to do if the two components were separated.

The naive implementation I was thinking of is where the main and cross functions would return a value (a reference to the whole Style struct I guess) wrapped in an enum:

enum Axis<T> {
  Row(T)
  Column(T)
}

and then the chained functions won't need an axis parameter, they can just match against the axis enum.

An alternative with Size might be to replace the min_size, max_size and max_size fields with:

pub struct LengthConstraints<T> {
    pub min: T,
    pub max: T,
    pub length: T,
}

pub struct Style {
    pub size: Size<LengthConstraints<Dimension>>,
    // .. rest of style properties
}

and then directly implement all the math, clamping, resolve, etc traits on Size<LengthConstraints<T>>

@ickshonpe
Copy link
Contributor

ickshonpe commented Feb 2, 2023

There should probably be a different type for border and padding. It doesn't make sense to assign either of them auto or undefined values.

@ickshonpe ickshonpe mentioned this pull request Feb 3, 2023
@alice-i-cecile
Copy link
Member Author

Closing out: this isn't going to be worth rebasing. I still think we should do this, but it's not sufficiently motivated until we have multiple layout algorithms.

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-Code-Quality A section of code that is hard to understand or change C-Usability A targeted quality-of-life change that makes Bevy easier to use M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it X-Controversial There is active debate or serious implications around merging this PR

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Split UI Style into multiple components [Minor] Rename or hide bevy_ui::ui_node::Direction