Add grid support to GPUI (#36153)

Mikayla Maki and Anthony created

Release Notes:

- N/A

---------

Co-authored-by: Anthony <anthony@zed.dev>

Change summary

crates/gpui/Cargo.toml              |   4 +
crates/gpui/examples/grid_layout.rs |  80 ++++++++++++++++++++++
crates/gpui/src/geometry.rs         |  33 +++++++++
crates/gpui/src/style.rs            |  23 ++++++
crates/gpui/src/styled.rs           | 109 ++++++++++++++++++++++++++++++
crates/gpui/src/taffy.rs            |  35 +++++++++
6 files changed, 278 insertions(+), 6 deletions(-)

Detailed changes

crates/gpui/Cargo.toml 🔗

@@ -305,3 +305,7 @@ path = "examples/uniform_list.rs"
 [[example]]
 name = "window_shadow"
 path = "examples/window_shadow.rs"
+
+[[example]]
+name = "grid_layout"
+path = "examples/grid_layout.rs"

crates/gpui/examples/grid_layout.rs 🔗

@@ -0,0 +1,80 @@
+use gpui::{
+    App, Application, Bounds, Context, Hsla, Window, WindowBounds, WindowOptions, div, prelude::*,
+    px, rgb, size,
+};
+
+// https://en.wikipedia.org/wiki/Holy_grail_(web_design)
+struct HolyGrailExample {}
+
+impl Render for HolyGrailExample {
+    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
+        let block = |color: Hsla| {
+            div()
+                .size_full()
+                .bg(color)
+                .border_1()
+                .border_dashed()
+                .rounded_md()
+                .border_color(gpui::white())
+                .items_center()
+        };
+
+        div()
+            .gap_1()
+            .grid()
+            .bg(rgb(0x505050))
+            .size(px(500.0))
+            .shadow_lg()
+            .border_1()
+            .size_full()
+            .grid_cols(5)
+            .grid_rows(5)
+            .child(
+                block(gpui::white())
+                    .row_span(1)
+                    .col_span_full()
+                    .child("Header"),
+            )
+            .child(
+                block(gpui::red())
+                    .col_span(1)
+                    .h_56()
+                    .child("Table of contents"),
+            )
+            .child(
+                block(gpui::green())
+                    .col_span(3)
+                    .row_span(3)
+                    .child("Content"),
+            )
+            .child(
+                block(gpui::blue())
+                    .col_span(1)
+                    .row_span(3)
+                    .child("AD :(")
+                    .text_color(gpui::white()),
+            )
+            .child(
+                block(gpui::black())
+                    .row_span(1)
+                    .col_span_full()
+                    .text_color(gpui::white())
+                    .child("Footer"),
+            )
+    }
+}
+
+fn main() {
+    Application::new().run(|cx: &mut App| {
+        let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx);
+        cx.open_window(
+            WindowOptions {
+                window_bounds: Some(WindowBounds::Windowed(bounds)),
+                ..Default::default()
+            },
+            |_, cx| cx.new(|_| HolyGrailExample {}),
+        )
+        .unwrap();
+        cx.activate(true);
+    });
+}

crates/gpui/src/geometry.rs 🔗

@@ -9,12 +9,14 @@ use refineable::Refineable;
 use schemars::{JsonSchema, json_schema};
 use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
 use std::borrow::Cow;
+use std::ops::Range;
 use std::{
     cmp::{self, PartialOrd},
     fmt::{self, Display},
     hash::Hash,
     ops::{Add, Div, Mul, MulAssign, Neg, Sub},
 };
+use taffy::prelude::{TaffyGridLine, TaffyGridSpan};
 
 use crate::{App, DisplayId};
 
@@ -3608,6 +3610,37 @@ impl From<()> for Length {
     }
 }
 
+/// A location in a grid layout.
+#[derive(Clone, PartialEq, Debug, Serialize, Deserialize, JsonSchema, Default)]
+pub struct GridLocation {
+    /// The rows this item uses within the grid.
+    pub row: Range<GridPlacement>,
+    /// The columns this item uses within the grid.
+    pub column: Range<GridPlacement>,
+}
+
+/// The placement of an item within a grid layout's column or row.
+#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, JsonSchema, Default)]
+pub enum GridPlacement {
+    /// The grid line index to place this item.
+    Line(i16),
+    /// The number of grid lines to span.
+    Span(u16),
+    /// Automatically determine the placement, equivalent to Span(1)
+    #[default]
+    Auto,
+}
+
+impl From<GridPlacement> for taffy::GridPlacement {
+    fn from(placement: GridPlacement) -> Self {
+        match placement {
+            GridPlacement::Line(index) => taffy::GridPlacement::from_line_index(index),
+            GridPlacement::Span(span) => taffy::GridPlacement::from_span(span),
+            GridPlacement::Auto => taffy::GridPlacement::Auto,
+        }
+    }
+}
+
 /// Provides a trait for types that can calculate half of their value.
 ///
 /// The `Half` trait is used for types that can be evenly divided, returning a new instance of the same type

crates/gpui/src/style.rs 🔗

@@ -7,7 +7,7 @@ use std::{
 use crate::{
     AbsoluteLength, App, Background, BackgroundTag, BorderStyle, Bounds, ContentMask, Corners,
     CornersRefinement, CursorStyle, DefiniteLength, DevicePixels, Edges, EdgesRefinement, Font,
-    FontFallbacks, FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point,
+    FontFallbacks, FontFeatures, FontStyle, FontWeight, GridLocation, Hsla, Length, Pixels, Point,
     PointRefinement, Rgba, SharedString, Size, SizeRefinement, Styled, TextRun, Window, black, phi,
     point, quad, rems, size,
 };
@@ -260,6 +260,17 @@ pub struct Style {
     /// The opacity of this element
     pub opacity: Option<f32>,
 
+    /// The grid columns of this element
+    /// Equivalent to the Tailwind `grid-cols-<number>`
+    pub grid_cols: Option<u16>,
+
+    /// The row span of this element
+    /// Equivalent to the Tailwind `grid-rows-<number>`
+    pub grid_rows: Option<u16>,
+
+    /// The grid location of this element
+    pub grid_location: Option<GridLocation>,
+
     /// Whether to draw a red debugging outline around this element
     #[cfg(debug_assertions)]
     pub debug: bool,
@@ -275,6 +286,13 @@ impl Styled for StyleRefinement {
     }
 }
 
+impl StyleRefinement {
+    /// The grid location of this element
+    pub fn grid_location_mut(&mut self) -> &mut GridLocation {
+        self.grid_location.get_or_insert_default()
+    }
+}
+
 /// The value of the visibility property, similar to the CSS property `visibility`
 #[derive(Default, Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
 pub enum Visibility {
@@ -757,6 +775,9 @@ impl Default for Style {
             text: TextStyleRefinement::default(),
             mouse_cursor: None,
             opacity: None,
+            grid_rows: None,
+            grid_cols: None,
+            grid_location: None,
 
             #[cfg(debug_assertions)]
             debug: false,

crates/gpui/src/styled.rs 🔗

@@ -1,8 +1,8 @@
 use crate::{
     self as gpui, AbsoluteLength, AlignContent, AlignItems, BorderStyle, CursorStyle,
-    DefiniteLength, Display, Fill, FlexDirection, FlexWrap, Font, FontStyle, FontWeight, Hsla,
-    JustifyContent, Length, SharedString, StrikethroughStyle, StyleRefinement, TextAlign,
-    TextOverflow, TextStyleRefinement, UnderlineStyle, WhiteSpace, px, relative, rems,
+    DefiniteLength, Display, Fill, FlexDirection, FlexWrap, Font, FontStyle, FontWeight,
+    GridPlacement, Hsla, JustifyContent, Length, SharedString, StrikethroughStyle, StyleRefinement,
+    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,
@@ -46,6 +46,13 @@ pub trait Styled: Sized {
         self
     }
 
+    /// Sets the display type of the element to `grid`.
+    /// [Docs](https://tailwindcss.com/docs/display)
+    fn grid(mut self) -> Self {
+        self.style().display = Some(Display::Grid);
+        self
+    }
+
     /// Sets the whitespace of the element to `normal`.
     /// [Docs](https://tailwindcss.com/docs/whitespace#normal)
     fn whitespace_normal(mut self) -> Self {
@@ -640,6 +647,102 @@ pub trait Styled: Sized {
         self
     }
 
+    /// Sets the grid columns of this element.
+    fn grid_cols(mut self, cols: u16) -> Self {
+        self.style().grid_cols = Some(cols);
+        self
+    }
+
+    /// Sets the grid rows of this element.
+    fn grid_rows(mut self, rows: u16) -> Self {
+        self.style().grid_rows = Some(rows);
+        self
+    }
+
+    /// Sets the column start of this element.
+    fn col_start(mut self, start: i16) -> Self {
+        let grid_location = self.style().grid_location_mut();
+        grid_location.column.start = GridPlacement::Line(start);
+        self
+    }
+
+    /// Sets the column start of this element to auto.
+    fn col_start_auto(mut self) -> Self {
+        let grid_location = self.style().grid_location_mut();
+        grid_location.column.start = GridPlacement::Auto;
+        self
+    }
+
+    /// Sets the column end of this element.
+    fn col_end(mut self, end: i16) -> Self {
+        let grid_location = self.style().grid_location_mut();
+        grid_location.column.end = GridPlacement::Line(end);
+        self
+    }
+
+    /// Sets the column end of this element to auto.
+    fn col_end_auto(mut self) -> Self {
+        let grid_location = self.style().grid_location_mut();
+        grid_location.column.end = GridPlacement::Auto;
+        self
+    }
+
+    /// Sets the column span of this element.
+    fn col_span(mut self, span: u16) -> Self {
+        let grid_location = self.style().grid_location_mut();
+        grid_location.column = GridPlacement::Span(span)..GridPlacement::Span(span);
+        self
+    }
+
+    /// Sets the row span of this element.
+    fn col_span_full(mut self) -> Self {
+        let grid_location = self.style().grid_location_mut();
+        grid_location.column = GridPlacement::Line(1)..GridPlacement::Line(-1);
+        self
+    }
+
+    /// Sets the row start of this element.
+    fn row_start(mut self, start: i16) -> Self {
+        let grid_location = self.style().grid_location_mut();
+        grid_location.row.start = GridPlacement::Line(start);
+        self
+    }
+
+    /// Sets the row start of this element to "auto"
+    fn row_start_auto(mut self) -> Self {
+        let grid_location = self.style().grid_location_mut();
+        grid_location.row.start = GridPlacement::Auto;
+        self
+    }
+
+    /// Sets the row end of this element.
+    fn row_end(mut self, end: i16) -> Self {
+        let grid_location = self.style().grid_location_mut();
+        grid_location.row.end = GridPlacement::Line(end);
+        self
+    }
+
+    /// Sets the row end of this element to "auto"
+    fn row_end_auto(mut self) -> Self {
+        let grid_location = self.style().grid_location_mut();
+        grid_location.row.end = GridPlacement::Auto;
+        self
+    }
+
+    /// Sets the row span of this element.
+    fn row_span(mut self, span: u16) -> Self {
+        let grid_location = self.style().grid_location_mut();
+        grid_location.row = GridPlacement::Span(span)..GridPlacement::Span(span);
+        self
+    }
+
+    /// Sets the row span of this element.
+    fn row_span_full(mut self) -> Self {
+        let grid_location = self.style().grid_location_mut();
+        grid_location.row = GridPlacement::Line(1)..GridPlacement::Line(-1);
+        self
+    }
+
     /// Draws a debug border around this element.
     #[cfg(debug_assertions)]
     fn debug(mut self) -> Self {

crates/gpui/src/taffy.rs 🔗

@@ -3,7 +3,7 @@ use crate::{
 };
 use collections::{FxHashMap, FxHashSet};
 use smallvec::SmallVec;
-use std::fmt::Debug;
+use std::{fmt::Debug, ops::Range};
 use taffy::{
     TaffyTree, TraversePartialTree as _,
     geometry::{Point as TaffyPoint, Rect as TaffyRect, Size as TaffySize},
@@ -251,6 +251,25 @@ trait ToTaffy<Output> {
 
 impl ToTaffy<taffy::style::Style> for Style {
     fn to_taffy(&self, rem_size: Pixels) -> taffy::style::Style {
+        use taffy::style_helpers::{fr, length, minmax, repeat};
+
+        fn to_grid_line(
+            placement: &Range<crate::GridPlacement>,
+        ) -> taffy::Line<taffy::GridPlacement> {
+            taffy::Line {
+                start: placement.start.into(),
+                end: placement.end.into(),
+            }
+        }
+
+        fn to_grid_repeat<T: taffy::style::CheapCloneStr>(
+            unit: &Option<u16>,
+        ) -> 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()
+        }
+
         taffy::style::Style {
             display: self.display.into(),
             overflow: self.overflow.into(),
@@ -274,7 +293,19 @@ impl ToTaffy<taffy::style::Style> for Style {
             flex_basis: self.flex_basis.to_taffy(rem_size),
             flex_grow: self.flex_grow,
             flex_shrink: self.flex_shrink,
-            ..Default::default() // Ignore grid properties for now
+            grid_template_rows: to_grid_repeat(&self.grid_rows),
+            grid_template_columns: to_grid_repeat(&self.grid_cols),
+            grid_row: self
+                .grid_location
+                .as_ref()
+                .map(|location| to_grid_line(&location.row))
+                .unwrap_or_default(),
+            grid_column: self
+                .grid_location
+                .as_ref()
+                .map(|location| to_grid_line(&location.column))
+                .unwrap_or_default(),
+            ..Default::default()
         }
     }
 }