Add `WithRemSize` element (#11928)

Marshall Bowers created

This PR adds a new `WithRemSize` element to the `ui` crate.

This element can be used to create an element tree that has a different
rem size than the base window.

`WithRemSize` can be nested, allowing for subtrees that have a different
rem size than their parent and their children.

<img width="912" alt="Screenshot 2024-05-16 at 2 25 28 PM"
src="https://github.com/zed-industries/zed/assets/1486634/f599cd9f-c101-496b-93e8-06e570fbf74f">

Release Notes:

- N/A

Change summary

crates/gpui/src/window.rs                     | 17 ++--
crates/storybook/src/stories.rs               |  2 
crates/storybook/src/stories/with_rem_size.rs | 61 +++++++++++++++++
crates/storybook/src/story_selector.rs        |  2 
crates/ui/src/ui.rs                           |  3 
crates/ui/src/with_rem_size.rs                | 75 +++++++++++++++++++++
6 files changed, 150 insertions(+), 10 deletions(-)

Detailed changes

crates/gpui/src/window.rs 🔗

@@ -491,14 +491,11 @@ pub struct Window {
     sprite_atlas: Arc<dyn PlatformAtlas>,
     text_system: Arc<WindowTextSystem>,
     rem_size: Pixels,
-    /// An override value for the window's rem size.
+    /// The stack of override values for the window's rem size.
     ///
     /// This is used by `with_rem_size` to allow rendering an element tree with
     /// a given rem size.
-    ///
-    /// Note: Right now we only allow for a single override value at a time, but
-    /// this could likely be changed to be a stack of rem sizes.
-    rem_size_override: Option<Pixels>,
+    rem_size_override_stack: SmallVec<[Pixels; 8]>,
     pub(crate) viewport_size: Size<Pixels>,
     layout_engine: Option<TaffyLayoutEngine>,
     pub(crate) root_view: Option<AnyView>,
@@ -771,7 +768,7 @@ impl Window {
             sprite_atlas,
             text_system,
             rem_size: px(16.),
-            rem_size_override: None,
+            rem_size_override_stack: SmallVec::new(),
             viewport_size: content_size,
             layout_engine: Some(TaffyLayoutEngine::new()),
             root_view: None,
@@ -1212,7 +1209,9 @@ impl<'a> WindowContext<'a> {
     /// UI to scale, just like zooming a web page.
     pub fn rem_size(&self) -> Pixels {
         self.window
-            .rem_size_override
+            .rem_size_override_stack
+            .last()
+            .copied()
             .unwrap_or(self.window.rem_size)
     }
 
@@ -1238,9 +1237,9 @@ impl<'a> WindowContext<'a> {
         );
 
         if let Some(rem_size) = rem_size {
-            self.window.rem_size_override = Some(rem_size.into());
+            self.window.rem_size_override_stack.push(rem_size.into());
             let result = f(self);
-            self.window.rem_size_override.take();
+            self.window.rem_size_override_stack.pop();
             result
         } else {
             f(self)

crates/storybook/src/stories.rs 🔗

@@ -7,6 +7,7 @@ mod picker;
 mod scroll;
 mod text;
 mod viewport_units;
+mod with_rem_size;
 
 pub use auto_height_editor::*;
 pub use cursor::*;
@@ -17,3 +18,4 @@ pub use picker::*;
 pub use scroll::*;
 pub use text::*;
 pub use viewport_units::*;
+pub use with_rem_size::*;

crates/storybook/src/stories/with_rem_size.rs 🔗

@@ -0,0 +1,61 @@
+use gpui::{AnyElement, Hsla, Render};
+use story::Story;
+
+use ui::{prelude::*, WithRemSize};
+
+pub struct WithRemSizeStory;
+
+impl Render for WithRemSizeStory {
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
+        Story::container().child(
+            Example::new(16., gpui::red())
+                .child(
+                    Example::new(24., gpui::green())
+                        .child(Example::new(8., gpui::blue()))
+                        .child(Example::new(16., gpui::yellow())),
+                )
+                .child(
+                    Example::new(12., gpui::green())
+                        .child(Example::new(48., gpui::blue()))
+                        .child(Example::new(16., gpui::yellow())),
+                ),
+        )
+    }
+}
+
+#[derive(IntoElement)]
+struct Example {
+    rem_size: Pixels,
+    border_color: Hsla,
+    children: Vec<AnyElement>,
+}
+
+impl Example {
+    pub fn new(rem_size: impl Into<Pixels>, border_color: Hsla) -> Self {
+        Self {
+            rem_size: rem_size.into(),
+            border_color,
+            children: Vec::new(),
+        }
+    }
+}
+
+impl ParentElement for Example {
+    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
+        self.children.extend(elements);
+    }
+}
+
+impl RenderOnce for Example {
+    fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
+        WithRemSize::new(self.rem_size).child(
+            v_flex()
+                .gap_2()
+                .p_2()
+                .border_2()
+                .border_color(self.border_color)
+                .child(Label::new(format!("1rem = {}px", self.rem_size.0)))
+                .children(self.children),
+        )
+    }
+}

crates/storybook/src/story_selector.rs 🔗

@@ -40,6 +40,7 @@ pub enum ComponentStory {
     ToggleButton,
     ToolStrip,
     ViewportUnits,
+    WithRemSize,
 }
 
 impl ComponentStory {
@@ -76,6 +77,7 @@ impl ComponentStory {
             Self::ToggleButton => cx.new_view(|_| ui::ToggleButtonStory).into(),
             Self::ToolStrip => cx.new_view(|_| ui::ToolStripStory).into(),
             Self::ViewportUnits => cx.new_view(|_| crate::stories::ViewportUnitsStory).into(),
+            Self::WithRemSize => cx.new_view(|_| crate::stories::WithRemSizeStory).into(),
             Self::Picker => PickerStory::new(cx).into(),
         }
     }

crates/ui/src/ui.rs 🔗

@@ -13,12 +13,13 @@ mod styled_ext;
 mod styles;
 pub mod utils;
 mod visible_on_hover;
+mod with_rem_size;
 
 pub use clickable::*;
 pub use components::*;
 pub use disableable::*;
 pub use fixed::*;
 pub use prelude::*;
-
 pub use styled_ext::*;
 pub use styles::*;
+pub use with_rem_size::*;

crates/ui/src/with_rem_size.rs 🔗

@@ -0,0 +1,75 @@
+use gpui::{
+    div, AnyElement, Bounds, Div, DivFrameState, Element, ElementId, GlobalElementId, Hitbox,
+    IntoElement, LayoutId, ParentElement, Pixels, WindowContext,
+};
+
+/// An element that sets a particular rem size for its children.
+pub struct WithRemSize {
+    div: Div,
+    rem_size: Pixels,
+}
+
+impl WithRemSize {
+    pub fn new(rem_size: impl Into<Pixels>) -> Self {
+        Self {
+            div: div(),
+            rem_size: rem_size.into(),
+        }
+    }
+}
+
+impl ParentElement for WithRemSize {
+    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
+        self.div.extend(elements)
+    }
+}
+
+impl Element for WithRemSize {
+    type RequestLayoutState = DivFrameState;
+    type PrepaintState = Option<Hitbox>;
+
+    fn id(&self) -> Option<ElementId> {
+        self.div.id()
+    }
+
+    fn request_layout(
+        &mut self,
+        id: Option<&GlobalElementId>,
+        cx: &mut WindowContext,
+    ) -> (LayoutId, Self::RequestLayoutState) {
+        cx.with_rem_size(Some(self.rem_size), |cx| self.div.request_layout(id, cx))
+    }
+
+    fn prepaint(
+        &mut self,
+        id: Option<&GlobalElementId>,
+        bounds: Bounds<Pixels>,
+        request_layout: &mut Self::RequestLayoutState,
+        cx: &mut WindowContext,
+    ) -> Self::PrepaintState {
+        cx.with_rem_size(Some(self.rem_size), |cx| {
+            self.div.prepaint(id, bounds, request_layout, cx)
+        })
+    }
+
+    fn paint(
+        &mut self,
+        id: Option<&GlobalElementId>,
+        bounds: Bounds<Pixels>,
+        request_layout: &mut Self::RequestLayoutState,
+        prepaint: &mut Self::PrepaintState,
+        cx: &mut WindowContext,
+    ) {
+        cx.with_rem_size(Some(self.rem_size), |cx| {
+            self.div.paint(id, bounds, request_layout, prepaint, cx)
+        })
+    }
+}
+
+impl IntoElement for WithRemSize {
+    type Element = Self;
+
+    fn into_element(self) -> Self::Element {
+        self
+    }
+}