Allow flex items to float to the end of the flex axis

Antonio Scandurra and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

crates/chat_panel/src/chat_panel.rs           |   2 
crates/contacts_panel/src/contacts_panel.rs   |   2 
crates/file_finder/src/file_finder.rs         |  36 +++---
crates/gpui/src/elements.rs                   |  11 +
crates/gpui/src/elements/flex.rs              | 118 ++++++++++++--------
crates/outline/src/outline.rs                 |   6 
crates/project_symbols/src/project_symbols.rs |   6 
crates/search/src/buffer_search.rs            |   6 
crates/search/src/project_search.rs           |  12 -
crates/theme_selector/src/theme_selector.rs   |   6 
crates/workspace/src/pane.rs                  |   4 
crates/workspace/src/pane_group.rs            |   2 
crates/workspace/src/sidebar.rs               |   2 
crates/workspace/src/status_bar.rs            |   2 
crates/workspace/src/toolbar.rs               |   2 
crates/workspace/src/workspace.rs             |  25 ++--
16 files changed, 138 insertions(+), 104 deletions(-)

Detailed changes

crates/chat_panel/src/chat_panel.rs 🔗

@@ -219,7 +219,7 @@ impl ChatPanel {
             Empty::new().boxed()
         };
 
-        Flexible::new(1., true, messages).boxed()
+        FlexItem::new(messages).flex(1., true).boxed()
     }
 
     fn render_message(&self, message: &ChannelMessage, cx: &AppContext) -> ElementBox {

crates/file_finder/src/file_finder.rs 🔗

@@ -78,7 +78,11 @@ impl View for FileFinder {
                                 .with_style(settings.theme.selector.input_editor.container)
                                 .boxed(),
                         )
-                        .with_child(Flexible::new(1.0, false, self.render_matches(cx)).boxed())
+                        .with_child(
+                            FlexItem::new(self.render_matches(cx))
+                                .flex(1., false)
+                                .boxed(),
+                        )
                         .boxed(),
                 )
                 .with_style(settings.theme.selector.container)
@@ -166,23 +170,19 @@ impl FileFinder {
                 //     .boxed(),
                 // )
                 .with_child(
-                    Flexible::new(
-                        1.0,
-                        false,
-                        Flex::column()
-                            .with_child(
-                                Label::new(file_name.to_string(), style.label.clone())
-                                    .with_highlights(file_name_positions)
-                                    .boxed(),
-                            )
-                            .with_child(
-                                Label::new(full_path, style.label.clone())
-                                    .with_highlights(full_path_positions)
-                                    .boxed(),
-                            )
-                            .boxed(),
-                    )
-                    .boxed(),
+                    Flex::column()
+                        .with_child(
+                            Label::new(file_name.to_string(), style.label.clone())
+                                .with_highlights(file_name_positions)
+                                .boxed(),
+                        )
+                        .with_child(
+                            Label::new(full_path, style.label.clone())
+                                .with_highlights(full_path_positions)
+                                .boxed(),
+                        )
+                        .flex(1., false)
+                        .boxed(),
                 )
                 .boxed(),
         )

crates/gpui/src/elements.rs 🔗

@@ -139,11 +139,18 @@ pub trait Element {
         Expanded::new(self.boxed())
     }
 
-    fn flexible(self, flex: f32, expanded: bool) -> Flexible
+    fn flex(self, flex: f32, expanded: bool) -> FlexItem
     where
         Self: 'static + Sized,
     {
-        Flexible::new(flex, expanded, self.boxed())
+        FlexItem::new(self.boxed()).flex(flex, expanded)
+    }
+
+    fn flex_float(self) -> FlexItem
+    where
+        Self: 'static + Sized,
+    {
+        FlexItem::new(self.boxed()).float()
     }
 }
 

crates/gpui/src/elements/flex.rs 🔗

@@ -34,7 +34,7 @@ impl Flex {
 
     fn layout_flex_children(
         &mut self,
-        expanded: bool,
+        layout_expanded: bool,
         constraint: SizeConstraint,
         remaining_space: &mut f32,
         remaining_flex: &mut f32,
@@ -44,32 +44,33 @@ impl Flex {
         let cross_axis = self.axis.invert();
         for child in &mut self.children {
             if let Some(metadata) = child.metadata::<FlexParentData>() {
-                if metadata.expanded != expanded {
-                    continue;
-                }
+                if let Some((flex, expanded)) = metadata.flex {
+                    if expanded != layout_expanded {
+                        continue;
+                    }
 
-                let flex = metadata.flex;
-                let child_max = if *remaining_flex == 0.0 {
-                    *remaining_space
-                } else {
-                    let space_per_flex = *remaining_space / *remaining_flex;
-                    space_per_flex * flex
-                };
-                let child_min = if expanded { child_max } else { 0. };
-                let child_constraint = match self.axis {
-                    Axis::Horizontal => SizeConstraint::new(
-                        vec2f(child_min, constraint.min.y()),
-                        vec2f(child_max, constraint.max.y()),
-                    ),
-                    Axis::Vertical => SizeConstraint::new(
-                        vec2f(constraint.min.x(), child_min),
-                        vec2f(constraint.max.x(), child_max),
-                    ),
-                };
-                let child_size = child.layout(child_constraint, cx);
-                *remaining_space -= child_size.along(self.axis);
-                *remaining_flex -= flex;
-                *cross_axis_max = cross_axis_max.max(child_size.along(cross_axis));
+                    let child_max = if *remaining_flex == 0.0 {
+                        *remaining_space
+                    } else {
+                        let space_per_flex = *remaining_space / *remaining_flex;
+                        space_per_flex * flex
+                    };
+                    let child_min = if expanded { child_max } else { 0. };
+                    let child_constraint = match self.axis {
+                        Axis::Horizontal => SizeConstraint::new(
+                            vec2f(child_min, constraint.min.y()),
+                            vec2f(child_max, constraint.max.y()),
+                        ),
+                        Axis::Vertical => SizeConstraint::new(
+                            vec2f(constraint.min.x(), child_min),
+                            vec2f(constraint.max.x(), child_max),
+                        ),
+                    };
+                    let child_size = child.layout(child_constraint, cx);
+                    *remaining_space -= child_size.along(self.axis);
+                    *remaining_flex -= flex;
+                    *cross_axis_max = cross_axis_max.max(child_size.along(cross_axis));
+                }
             }
         }
     }
@@ -82,7 +83,7 @@ impl Extend<ElementBox> for Flex {
 }
 
 impl Element for Flex {
-    type LayoutState = bool;
+    type LayoutState = f32;
     type PaintState = ();
 
     fn layout(
@@ -96,8 +97,11 @@ impl Element for Flex {
         let cross_axis = self.axis.invert();
         let mut cross_axis_max: f32 = 0.0;
         for child in &mut self.children {
-            if let Some(metadata) = child.metadata::<FlexParentData>() {
-                *total_flex.get_or_insert(0.) += metadata.flex;
+            if let Some(flex) = child
+                .metadata::<FlexParentData>()
+                .and_then(|metadata| metadata.flex.map(|(flex, _)| flex))
+            {
+                *total_flex.get_or_insert(0.) += flex;
             } else {
                 let child_constraint = match self.axis {
                     Axis::Horizontal => SizeConstraint::new(
@@ -115,12 +119,12 @@ impl Element for Flex {
             }
         }
 
+        let mut remaining_space = constraint.max_along(self.axis) - fixed_space;
         let mut size = if let Some(mut remaining_flex) = total_flex {
-            if constraint.max_along(self.axis).is_infinite() {
+            if remaining_space.is_infinite() {
                 panic!("flex contains flexible children but has an infinite constraint along the flex axis");
             }
 
-            let mut remaining_space = constraint.max_along(self.axis) - fixed_space;
             self.layout_flex_children(
                 false,
                 constraint,
@@ -156,38 +160,47 @@ impl Element for Flex {
             size.set_y(size.y().max(constraint.min.y()));
         }
 
-        let mut overflowing = false;
         if size.x() > constraint.max.x() {
             size.set_x(constraint.max.x());
-            overflowing = true;
         }
         if size.y() > constraint.max.y() {
             size.set_y(constraint.max.y());
-            overflowing = true;
         }
 
-        (size, overflowing)
+        (size, remaining_space)
     }
 
     fn paint(
         &mut self,
         bounds: RectF,
         visible_bounds: RectF,
-        overflowing: &mut Self::LayoutState,
+        remaining_space: &mut Self::LayoutState,
         cx: &mut PaintContext,
     ) -> Self::PaintState {
-        if *overflowing {
+        let overflowing = *remaining_space < 0.;
+        if overflowing {
             cx.scene.push_layer(Some(bounds));
         }
         let mut child_origin = bounds.origin();
         for child in &mut self.children {
+            if *remaining_space > 0. {
+                if let Some(metadata) = child.metadata::<FlexParentData>() {
+                    if metadata.float {
+                        match self.axis {
+                            Axis::Horizontal => child_origin += vec2f(*remaining_space, 0.0),
+                            Axis::Vertical => child_origin += vec2f(0.0, *remaining_space),
+                        }
+                        *remaining_space = 0.;
+                    }
+                }
+            }
             child.paint(child_origin, visible_bounds, cx);
             match self.axis {
                 Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0),
                 Axis::Vertical => child_origin += vec2f(0.0, child.size().y()),
             }
         }
-        if *overflowing {
+        if overflowing {
             cx.scene.pop_layer();
         }
     }
@@ -224,25 +237,38 @@ impl Element for Flex {
 }
 
 struct FlexParentData {
-    flex: f32,
-    expanded: bool,
+    flex: Option<(f32, bool)>,
+    float: bool,
 }
 
-pub struct Flexible {
+pub struct FlexItem {
     metadata: FlexParentData,
     child: ElementBox,
 }
 
-impl Flexible {
-    pub fn new(flex: f32, expanded: bool, child: ElementBox) -> Self {
-        Flexible {
-            metadata: FlexParentData { flex, expanded },
+impl FlexItem {
+    pub fn new(child: ElementBox) -> Self {
+        FlexItem {
+            metadata: FlexParentData {
+                flex: None,
+                float: false,
+            },
             child,
         }
     }
+
+    pub fn flex(mut self, flex: f32, expanded: bool) -> Self {
+        self.metadata.flex = Some((flex, expanded));
+        self
+    }
+
+    pub fn float(mut self) -> Self {
+        self.metadata.float = true;
+        self
+    }
 }
 
-impl Element for Flexible {
+impl Element for FlexItem {
     type LayoutState = ();
     type PaintState = ();
 

crates/outline/src/outline.rs 🔗

@@ -77,7 +77,11 @@ impl View for OutlineView {
                     .with_style(settings.theme.selector.input_editor.container)
                     .boxed(),
             )
-            .with_child(Flexible::new(1.0, false, self.render_matches(cx)).boxed())
+            .with_child(
+                FlexItem::new(self.render_matches(cx))
+                    .flex(1.0, false)
+                    .boxed(),
+            )
             .contained()
             .with_style(settings.theme.selector.container)
             .constrained()

crates/project_symbols/src/project_symbols.rs 🔗

@@ -76,7 +76,11 @@ impl View for ProjectSymbolsView {
                     .with_style(settings.theme.selector.input_editor.container)
                     .boxed(),
             )
-            .with_child(Flexible::new(1.0, false, self.render_matches(cx)).boxed())
+            .with_child(
+                FlexItem::new(self.render_matches(cx))
+                    .flex(1., false)
+                    .boxed(),
+            )
             .contained()
             .with_style(settings.theme.selector.container)
             .constrained()

crates/search/src/buffer_search.rs 🔗

@@ -82,11 +82,7 @@ impl View for BufferSearchBar {
             Flex::row()
                 .with_child(
                     Flex::row()
-                        .with_child(
-                            ChildView::new(&self.query_editor)
-                                .flexible(1., true)
-                                .boxed(),
-                        )
+                        .with_child(ChildView::new(&self.query_editor).flex(1., true).boxed())
                         .with_children(self.active_editor.as_ref().and_then(|editor| {
                             let matches = self.editors_with_matches.get(&editor.downgrade())?;
                             let message = if let Some(match_ix) = self.active_match_index {

crates/search/src/project_search.rs 🔗

@@ -164,12 +164,10 @@ impl View for ProjectSearchView {
                 .aligned()
                 .contained()
                 .with_background_color(theme.editor.background)
-                .flexible(1., true)
+                .flex(1., true)
                 .boxed()
         } else {
-            ChildView::new(&self.results_editor)
-                .flexible(1., true)
-                .boxed()
+            ChildView::new(&self.results_editor).flex(1., true).boxed()
         }
     }
 
@@ -691,11 +689,7 @@ impl View for ProjectSearchBar {
             Flex::row()
                 .with_child(
                     Flex::row()
-                        .with_child(
-                            ChildView::new(&search.query_editor)
-                                .flexible(1., true)
-                                .boxed(),
-                        )
+                        .with_child(ChildView::new(&search.query_editor).flex(1., true).boxed())
                         .with_children(search.active_match_index.map(|match_ix| {
                             Label::new(
                                 format!(

crates/theme_selector/src/theme_selector.rs 🔗

@@ -310,7 +310,11 @@ impl View for ThemeSelector {
                                 .with_style(theme.selector.input_editor.container)
                                 .boxed(),
                         )
-                        .with_child(Flexible::new(1.0, false, self.render_matches(cx)).boxed())
+                        .with_child(
+                            FlexItem::new(self.render_matches(cx))
+                                .flex(1., false)
+                                .boxed(),
+                        )
                         .boxed(),
                 )
                 .with_style(theme.selector.container)

crates/workspace/src/pane.rs 🔗

@@ -612,7 +612,7 @@ impl Pane {
                 Empty::new()
                     .contained()
                     .with_border(theme.workspace.tab.container.border)
-                    .flexible(0., true)
+                    .flex(0., true)
                     .named("filler"),
             );
 
@@ -641,7 +641,7 @@ impl View for Pane {
             Flex::column()
                 .with_child(self.render_tabs(cx))
                 .with_child(ChildView::new(&self.toolbar).boxed())
-                .with_child(ChildView::new(active_item).flexible(1., true).boxed())
+                .with_child(ChildView::new(active_item).flex(1., true).boxed())
                 .boxed()
         } else {
             Empty::new().boxed()

crates/workspace/src/pane_group.rs 🔗

@@ -248,7 +248,7 @@ impl PaneAxis {
                     member = Container::new(member).with_border(border).boxed();
                 }
 
-                Flexible::new(1.0, true, member).boxed()
+                FlexItem::new(member).flex(1.0, true).boxed()
             }))
             .boxed()
     }

crates/workspace/src/sidebar.rs 🔗

@@ -138,7 +138,7 @@ impl Sidebar {
                     let width = self.width.clone();
                     move |size, _| *width.borrow_mut() = size.x()
                 })
-                .flexible(1., false)
+                .flex(1., false)
                 .boxed(),
             );
             if matches!(self.side, Side::Left) {

crates/workspace/src/status_bar.rs 🔗

@@ -47,12 +47,12 @@ impl View for StatusBar {
                     .with_margin_right(theme.item_spacing)
                     .boxed()
             }))
-            .with_child(Empty::new().flexible(1., true).boxed())
             .with_children(self.right_items.iter().map(|i| {
                 ChildView::new(i.as_ref())
                     .aligned()
                     .contained()
                     .with_margin_left(theme.item_spacing)
+                    .flex_float()
                     .boxed()
             }))
             .contained()

crates/workspace/src/toolbar.rs 🔗

@@ -46,12 +46,12 @@ impl View for Toolbar {
                     .with_margin_right(theme.item_spacing)
                     .boxed()
             }))
-            .with_child(Empty::new().flexible(1., true).boxed())
             .with_children(self.right_items.iter().map(|i| {
                 ChildView::new(i.as_ref())
                     .aligned()
                     .contained()
                     .with_margin_left(theme.item_spacing)
+                    .flex_float()
                     .boxed()
             }))
             .contained()

crates/workspace/src/workspace.rs 🔗

@@ -1946,36 +1946,35 @@ impl View for Workspace {
                                 if let Some(element) =
                                     self.left_sidebar.render_active_item(&theme, cx)
                                 {
-                                    content.add_child(Flexible::new(0.8, false, element).boxed());
+                                    content
+                                        .add_child(FlexItem::new(element).flex(0.8, false).boxed());
                                 }
                                 content.add_child(
                                     Flex::column()
                                         .with_child(
-                                            Flexible::new(
-                                                1.,
-                                                true,
-                                                self.center.render(
-                                                    &theme,
-                                                    &self.follower_states_by_leader,
-                                                    self.project.read(cx).collaborators(),
-                                                ),
-                                            )
+                                            FlexItem::new(self.center.render(
+                                                &theme,
+                                                &self.follower_states_by_leader,
+                                                self.project.read(cx).collaborators(),
+                                            ))
+                                            .flex(1., true)
                                             .boxed(),
                                         )
                                         .with_child(ChildView::new(&self.status_bar).boxed())
-                                        .flexible(1., true)
+                                        .flex(1., true)
                                         .boxed(),
                                 );
                                 if let Some(element) =
                                     self.right_sidebar.render_active_item(&theme, cx)
                                 {
-                                    content.add_child(Flexible::new(0.8, false, element).boxed());
+                                    content
+                                        .add_child(FlexItem::new(element).flex(0.8, false).boxed());
                                 }
                                 content.add_child(self.right_sidebar.render(&theme, cx));
                                 content.boxed()
                             })
                             .with_children(self.modal.as_ref().map(|m| ChildView::new(m).boxed()))
-                            .flexible(1.0, true)
+                            .flex(1.0, true)
                             .boxed(),
                     )
                     .contained()