Add child item alignment to flex implementation

Mikayla Maki and Nathan created

Fix checkbox styling

co-authored-by: Nathan <nathan@zed.dev>

Change summary

crates/gpui/src/elements/flex.rs | 32 +++++++++++++++++++++++++
crates/theme/src/theme.rs        |  2 +
crates/welcome/src/welcome.rs    | 16 +++++++++---
styles/src/styleTree/welcome.ts  | 41 ++++++++++++++++++++++-----------
4 files changed, 72 insertions(+), 19 deletions(-)

Detailed changes

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

@@ -22,6 +22,7 @@ pub struct Flex {
     axis: Axis,
     children: Vec<ElementBox>,
     scroll_state: Option<(ElementStateHandle<Rc<ScrollState>>, usize)>,
+    child_alignment: f32,
 }
 
 impl Flex {
@@ -30,6 +31,7 @@ impl Flex {
             axis,
             children: Default::default(),
             scroll_state: None,
+            child_alignment: -1.,
         }
     }
 
@@ -41,6 +43,15 @@ impl Flex {
         Self::new(Axis::Vertical)
     }
 
+    /// Render children centered relative to the cross-axis of the parent flex.
+    ///
+    /// If this is a flex row, children will be centered vertically. If this is a
+    /// flex column, children will be centered horizontally.
+    pub fn align_children_center(mut self) -> Self {
+        self.child_alignment = 0.;
+        self
+    }
+
     pub fn scrollable<Tag, V>(
         mut self,
         element_id: usize,
@@ -309,7 +320,26 @@ impl Element for Flex {
                 }
             }
 
-            child.paint(child_origin, visible_bounds, cx);
+            // We use the child_alignment f32 to determine a point along the cross axis of the
+            // overall flex element and each child. We then align these points. So 0 would center
+            // each child relative to the overall height/width of the flex. -1 puts children at
+            // the start. 1 puts children at the end.
+            let cross_axis = self.axis.invert();
+            let my_center = bounds.size().along(cross_axis) / 2.;
+            let my_target = my_center + my_center * self.child_alignment;
+
+            let child_center = child.size().along(cross_axis) / 2.;
+            let child_target = child_center + child_center * self.child_alignment;
+
+            let mut aligned_child_origin = child_origin;
+            match self.axis {
+                Axis::Horizontal => aligned_child_origin
+                    .set_y(aligned_child_origin.y() - (child_target - my_target)),
+                Axis::Vertical => aligned_child_origin
+                    .set_x(aligned_child_origin.x() - (child_target - my_target)),
+            }
+
+            child.paint(aligned_child_origin, visible_bounds, cx);
 
             match self.axis {
                 Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0),

crates/theme/src/theme.rs 🔗

@@ -859,6 +859,8 @@ pub struct WelcomeStyle {
 
 #[derive(Clone, Deserialize, Default)]
 pub struct CheckboxStyle {
+    pub icon: String,
+    pub icon_color: Color,
     pub width: f32,
     pub height: f32,
     pub default: ContainerStyle,

crates/welcome/src/welcome.rs 🔗

@@ -1,7 +1,7 @@
 use std::borrow::Cow;
 
 use gpui::{
-    elements::{Canvas, Empty, Flex, Image, Label, MouseEventHandler, ParentElement, Stack},
+    elements::{Canvas, Empty, Flex, Image, Label, MouseEventHandler, ParentElement, Stack, Svg},
     geometry::rect::RectF,
     Action, Element, ElementBox, Entity, MouseButton, MouseRegion, MutableAppContext,
     RenderContext, Subscription, View, ViewContext,
@@ -104,6 +104,7 @@ impl View for WelcomePage {
                                 )
                                 .boxed(),
                             ])
+                            .align_children_center()
                             .boxed(),
                         Flex::row()
                             .with_children([
@@ -119,9 +120,9 @@ impl View for WelcomePage {
                                 )
                                 .boxed(),
                             ])
+                            .align_children_center()
                             .boxed(),
                     ])
-                    .aligned()
                     .boxed(),
             )
             .boxed()
@@ -177,8 +178,15 @@ impl WelcomePage {
         set_value: fn(&mut SettingsFileContent, checked: bool) -> (),
     ) -> ElementBox {
         MouseEventHandler::<T>::new(0, cx, |state, _| {
-            Empty::new()
-                .constrained()
+            let indicator = if checked {
+                Svg::new(style.icon.clone())
+                    .with_color(style.icon_color)
+                    .constrained()
+            } else {
+                Empty::new().constrained()
+            };
+
+            indicator
                 .with_width(style.width)
                 .with_height(style.height)
                 .contained()

styles/src/styleTree/welcome.ts 🔗

@@ -1,6 +1,6 @@
 
 import { ColorScheme } from "../themes/common/colorScheme";
-import { border, background, text } from "./components";
+import { border, background, foreground, text } from "./components";
 
 
 export default function welcome(colorScheme: ColorScheme) {
@@ -10,15 +10,18 @@ export default function welcome(colorScheme: ColorScheme) {
   let checkboxBase = {
     cornerRadius: 4,
     padding: {
-      left: 8,
-      right: 8,
-      top: 4,
-      bottom: 4,
+      left: 3,
+      right: 3,
+      top: 3,
+      bottom: 3,
     },
     shadow: colorScheme.popoverShadow,
     border: border(layer),
     margin: {
-      left: -8,
+      left: 8,
+      right: 8,
+      top: 5,
+      bottom: 5
     },
   };
 
@@ -44,30 +47,40 @@ export default function welcome(colorScheme: ColorScheme) {
       },
     },
     checkbox: {
-      width: 9,
-      height: 9,
+      width: 12,
+      height: 12,
+      icon: "icons/check_12.svg",
+      iconColor: foreground(layer, "on"),
       default: {
         ...checkboxBase,
-        background: colorScheme.ramps.blue(0.5).hex(),
+        background: background(layer, "default"),
+        border: {
+          color: foreground(layer, "hovered"),
+          width: 1,
+        }
       },
       checked: {
         ...checkboxBase,
-        background: colorScheme.ramps.red(0.5).hex(),
+        background: background(layer, "hovered"),
+        border: {
+          color: foreground(layer, "hovered"),
+          width: 1,
+        }
       },
       hovered: {
         ...checkboxBase,
-        background: colorScheme.ramps.blue(0.5).hex(),
+        background: background(layer, "hovered"),
 
         border: {
-          color: colorScheme.ramps.green(0.5).hex(),
+          color: foreground(layer, "hovered"),
           width: 1,
         }
       },
       hoveredAndChecked: {
         ...checkboxBase,
-        background: colorScheme.ramps.red(0.5).hex(),
+        background: background(layer, "hovered"),
         border: {
-          color: colorScheme.ramps.green(0.5).hex(),
+          color: foreground(layer, "hovered"),
           width: 1,
         }
       }