gpui: Add `grid_cols_max_content`for content-based column widths (#50839)

Abhiraj Damodare and Mikayla Maki created

Summary
Add a new grid_cols_max_content GPUI styling API that uses minmax(0,
max-content) for grid column sizing. This allows columns to
automatically size based on their content width while remaining
responsive when the container shrinks.

Applied the fix to both markdown preview (markdown_renderer.rs) and
agent panel (markdown.rs) table rendering. Table borders now wrap
tightly around content instead of stretching to full container width.

Fixes #50044

Approach
A new grid_cols_max_content API is added (as discussed with
@MikaylaMaki):

style.rs — New grid_cols_max_content: Option<u16> field
styled.rs — New .grid_cols_max_content(cols) builder method
taffy.rs — New to_grid_repeat_max_content() using minmax(0, max-content)
markdown_renderer.rs — Swapped .grid_cols() → .grid_cols_max_content(),
moved border to grid div, wrapped in v_flex().items_start() so border
hugs content
markdown.rs — Applied same fix for agent panel tables:
grid_cols_max_content, border on grid div, wrapped in
div().flex().flex_col().items_start() container
Screenshots
Before (equal-width columns, border stretches full width):

<img width="1386" height="890" alt="Screenshot 2026-03-06 at 2 17 54 PM"
src="https://github.com/user-attachments/assets/42cf76c4-6eba-4919-9b16-78c7fc823315"
/>

<img width="2555" height="1308" alt="original issue"
src="https://github.com/user-attachments/assets/22b0fc02-5203-48bb-8f03-7aa8255197cc"
/>



After — Markdown Preview and Agent Panel
<img width="2554" height="1317" alt="Screenshot 2026-03-07 at 2 29
28 PM"
src="https://github.com/user-attachments/assets/8849988e-9ba8-4388-9c29-a255e0ecc52b"
/>


Before you mark this PR as ready for review, make sure that you have:

Added a solid test coverage and/or screenshots from doing manual testing
 Done a self-review taking into account security and performance aspects
 Aligned any UI changes with the UI checklist
Release Notes:

Fixed markdown table columns to use content-based auto-width instead of
equal-width distribution in both markdown preview and agent panel
(#50044).

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>

Change summary

crates/gpui/src/style.rs                         | 47 ++++++++++++++---
crates/gpui/src/styled.rs                        | 30 +++++++++--
crates/gpui/src/taffy.rs                         | 48 ++++++++++-------
crates/markdown/src/markdown.rs                  | 10 +++
crates/markdown_preview/src/markdown_renderer.rs | 11 +--
5 files changed, 104 insertions(+), 42 deletions(-)

Detailed changes

crates/gpui/src/style.rs 🔗

@@ -138,6 +138,42 @@ impl ObjectFit {
     }
 }
 
+/// The minimum size of a column or row in a grid layout
+#[derive(
+    Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Default, JsonSchema, Serialize, Deserialize,
+)]
+pub enum TemplateColumnMinSize {
+    /// The column size may be 0
+    #[default]
+    Zero,
+    /// The column size can be determined by the min content
+    MinContent,
+    /// The column size can be determined by the max content
+    MaxContent,
+}
+
+/// A simplified representation of the grid-template-* value
+#[derive(
+    Copy,
+    Clone,
+    Refineable,
+    PartialEq,
+    Eq,
+    PartialOrd,
+    Ord,
+    Debug,
+    Default,
+    JsonSchema,
+    Serialize,
+    Deserialize,
+)]
+pub struct GridTemplate {
+    /// How this template directive should be repeated
+    pub repeat: u16,
+    /// The minimum size in the repeat(<>, minmax(_, 1fr)) equation
+    pub min_size: TemplateColumnMinSize,
+}
+
 /// The CSS styling that can be applied to an element via the `Styled` trait
 #[derive(Clone, Refineable, Debug)]
 #[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
@@ -262,16 +298,12 @@ pub struct Style {
     pub opacity: Option<f32>,
 
     /// The grid columns of this element
-    /// Equivalent to the Tailwind `grid-cols-<number>`
-    pub grid_cols: Option<u16>,
-
-    /// The grid columns with min-content minimum sizing.
-    /// Unlike grid_cols, it won't shrink to width 0 in AvailableSpace::MinContent constraints.
-    pub grid_cols_min_content: Option<u16>,
+    /// Roughly equivalent to the Tailwind `grid-cols-<number>`
+    pub grid_cols: Option<GridTemplate>,
 
     /// The row span of this element
     /// Equivalent to the Tailwind `grid-rows-<number>`
-    pub grid_rows: Option<u16>,
+    pub grid_rows: Option<GridTemplate>,
 
     /// The grid location of this element
     pub grid_location: Option<GridLocation>,
@@ -790,7 +822,6 @@ impl Default for Style {
             opacity: None,
             grid_rows: None,
             grid_cols: None,
-            grid_cols_min_content: None,
             grid_location: None,
 
             #[cfg(debug_assertions)]

crates/gpui/src/styled.rs 🔗

@@ -1,9 +1,9 @@
 use crate::{
     self as gpui, AbsoluteLength, AlignContent, AlignItems, AlignSelf, BorderStyle, CursorStyle,
     DefiniteLength, Display, Fill, FlexDirection, FlexWrap, Font, FontFeatures, FontStyle,
-    FontWeight, GridPlacement, Hsla, JustifyContent, Length, SharedString, StrikethroughStyle,
-    StyleRefinement, TextAlign, TextOverflow, TextStyleRefinement, UnderlineStyle, WhiteSpace, px,
-    relative, rems,
+    FontWeight, GridPlacement, GridTemplate, Hsla, JustifyContent, Length, SharedString,
+    StrikethroughStyle, StyleRefinement, TemplateColumnMinSize, TextAlign, TextOverflow,
+    TextStyleRefinement, UnderlineStyle, WhiteSpace, px, relative, rems,
 };
 pub use gpui_macros::{
     border_style_methods, box_shadow_style_methods, cursor_style_methods, margin_style_methods,
@@ -711,20 +711,38 @@ pub trait Styled: Sized {
 
     /// Sets the grid columns of this element.
     fn grid_cols(mut self, cols: u16) -> Self {
-        self.style().grid_cols = Some(cols);
+        self.style().grid_cols = Some(GridTemplate {
+            repeat: cols,
+            min_size: TemplateColumnMinSize::Zero,
+        });
         self
     }
 
     /// Sets the grid columns with min-content minimum sizing.
     /// Unlike grid_cols, it won't shrink to width 0 in AvailableSpace::MinContent constraints.
     fn grid_cols_min_content(mut self, cols: u16) -> Self {
-        self.style().grid_cols_min_content = Some(cols);
+        self.style().grid_cols = Some(GridTemplate {
+            repeat: cols,
+            min_size: TemplateColumnMinSize::MinContent,
+        });
+        self
+    }
+
+    /// Sets the grid columns with max-content maximum sizing for content-based column widths.
+    fn grid_cols_max_content(mut self, cols: u16) -> Self {
+        self.style().grid_cols = Some(GridTemplate {
+            repeat: cols,
+            min_size: TemplateColumnMinSize::MaxContent,
+        });
         self
     }
 
     /// Sets the grid rows of this element.
     fn grid_rows(mut self, rows: u16) -> Self {
-        self.style().grid_rows = Some(rows);
+        self.style().grid_rows = Some(GridTemplate {
+            repeat: rows,
+            min_size: TemplateColumnMinSize::Zero,
+        });
         self
     }
 

crates/gpui/src/taffy.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
-    AbsoluteLength, App, Bounds, DefiniteLength, Edges, Length, Pixels, Point, Size, Style, Window,
-    point, size,
+    AbsoluteLength, App, Bounds, DefiniteLength, Edges, GridTemplate, Length, Pixels, Point, Size,
+    Style, Window, point, size,
 };
 use collections::{FxHashMap, FxHashSet};
 use stacksafe::{StackSafe, stacksafe};
@@ -8,7 +8,7 @@ use std::{fmt::Debug, ops::Range};
 use taffy::{
     TaffyTree, TraversePartialTree as _,
     geometry::{Point as TaffyPoint, Rect as TaffyRect, Size as TaffySize},
-    prelude::min_content,
+    prelude::{max_content, min_content},
     style::AvailableSpace as TaffyAvailableSpace,
     tree::NodeId,
 };
@@ -308,19 +308,31 @@ impl ToTaffy<taffy::style::Style> for Style {
         }
 
         fn to_grid_repeat<T: taffy::style::CheapCloneStr>(
-            unit: &Option<u16>,
+            unit: &Option<GridTemplate>,
         ) -> Vec<taffy::GridTemplateComponent<T>> {
-            // grid-template-columns: repeat(<number>, minmax(0, 1fr));
-            unit.map(|count| vec![repeat(count, vec![minmax(length(0.0), fr(1.0))])])
-                .unwrap_or_default()
-        }
-
-        fn to_grid_repeat_min_content<T: taffy::style::CheapCloneStr>(
-            unit: &Option<u16>,
-        ) -> Vec<taffy::GridTemplateComponent<T>> {
-            // grid-template-columns: repeat(<number>, minmax(min-content, 1fr));
-            unit.map(|count| vec![repeat(count, vec![minmax(min_content(), fr(1.0))])])
-                .unwrap_or_default()
+            unit.map(|template| {
+                match template.min_size {
+                    // grid-template-*: repeat(<number>, minmax(0, 1fr));
+                    crate::TemplateColumnMinSize::Zero => {
+                        vec![repeat(template.repeat, vec![minmax(length(0.0), fr(1.0))])]
+                    }
+                    // grid-template-*: repeat(<number>, minmax(min-content, 1fr));
+                    crate::TemplateColumnMinSize::MinContent => {
+                        vec![repeat(
+                            template.repeat,
+                            vec![minmax(min_content(), fr(1.0))],
+                        )]
+                    }
+                    // grid-template-*: repeat(<number>, minmax(0, max-content))
+                    crate::TemplateColumnMinSize::MaxContent => {
+                        vec![repeat(
+                            template.repeat,
+                            vec![minmax(length(0.0), max_content())],
+                        )]
+                    }
+                }
+            })
+            .unwrap_or_default()
         }
 
         taffy::style::Style {
@@ -347,11 +359,7 @@ impl ToTaffy<taffy::style::Style> for Style {
             flex_grow: self.flex_grow,
             flex_shrink: self.flex_shrink,
             grid_template_rows: to_grid_repeat(&self.grid_rows),
-            grid_template_columns: if self.grid_cols_min_content.is_some() {
-                to_grid_repeat_min_content(&self.grid_cols_min_content)
-            } else {
-                to_grid_repeat(&self.grid_cols)
-            },
+            grid_template_columns: to_grid_repeat(&self.grid_cols),
             grid_row: self
                 .grid_location
                 .as_ref()

crates/markdown/src/markdown.rs 🔗

@@ -1271,18 +1271,23 @@ impl Element for MarkdownElement {
                             builder.table.start(alignments.clone());
 
                             let column_count = alignments.len();
+                            builder.push_div(
+                                div().flex().flex_col().items_start(),
+                                range,
+                                markdown_end,
+                            );
                             builder.push_div(
                                 div()
                                     .id(("table", range.start))
+                                    .min_w_0()
                                     .grid()
                                     .grid_cols(column_count as u16)
                                     .when(self.style.table_columns_min_size, |this| {
                                         this.grid_cols_min_content(column_count as u16)
                                     })
                                     .when(!self.style.table_columns_min_size, |this| {
-                                        this.grid_cols(column_count as u16)
+                                        this.grid_cols_max_content(column_count as u16)
                                     })
-                                    .w_full()
                                     .mb_2()
                                     .border(px(1.5))
                                     .border_color(cx.theme().colors().border)
@@ -1430,6 +1435,7 @@ impl Element for MarkdownElement {
                         }
                     }
                     MarkdownTagEnd::Table => {
+                        builder.pop_div();
                         builder.pop_div();
                         builder.table.end();
                     }

crates/markdown_preview/src/markdown_renderer.rs 🔗

@@ -698,16 +698,15 @@ fn render_markdown_table(parsed: &ParsedMarkdownTable, cx: &mut RenderContext) -
         .when_some(parsed.caption.as_ref(), |this, caption| {
             this.children(render_markdown_text(caption, cx))
         })
-        .border_1()
-        .border_color(cx.border_color)
-        .rounded_sm()
-        .overflow_hidden()
         .child(
             div()
+                .rounded_sm()
+                .overflow_hidden()
+                .border_1()
+                .border_color(cx.border_color)
                 .min_w_0()
-                .w_full()
                 .grid()
-                .grid_cols(max_column_count as u16)
+                .grid_cols_max_content(max_column_count as u16)
                 .children(cells),
         )
         .into_any()