Encode rem values derived from pixels using `rems_from_px` (#9367)

Marshall Bowers created

This PR adds a new `rems_from_px` helper function that can be used to
compute rem values based on a pixel value.

This is something we do fairly commonly, where we want to express a size
that is a given pixel size at the base rem size (e.g., "14px when the
rem size is 16px").

`rems_from_px` helps make the intent more explicit, as well as prevent
the base rem size from being duplicated everywhere.

Note: Ideally we would want `rems_from_px` to be `const`, but that
depends on https://github.com/rust-lang/rust/issues/57241.

Release Notes:

- N/A

Change summary

crates/breadcrumbs/src/breadcrumbs.rs                            |  2 
crates/collab_ui/src/collab_panel/channel_modal.rs               |  2 
crates/extensions_ui/src/extensions_ui.rs                        |  2 
crates/ui/src/components/avatar/avatar_audio_status_indicator.rs |  4 
crates/ui/src/components/button/button_like.rs                   | 10 
crates/ui/src/components/icon.rs                                 | 10 
crates/ui/src/components/keybinding.rs                           | 11 
crates/ui/src/components/popover_menu.rs                         |  6 
crates/ui/src/components/tab.rs                                  | 10 
crates/ui/src/components/tab_bar.rs                              |  4 
crates/ui/src/prelude.rs                                         |  2 
crates/ui/src/styles/typography.rs                               | 10 
crates/ui/src/styles/units.rs                                    | 16 +
13 files changed, 54 insertions(+), 35 deletions(-)

Detailed changes

crates/breadcrumbs/src/breadcrumbs.rs 🔗

@@ -86,7 +86,7 @@ impl Render for Breadcrumbs {
             ),
             None => element
                 // Match the height of the `ButtonLike` in the other arm.
-                .h(rems(22. / 16.))
+                .h(rems_from_px(22.))
                 .child(breadcrumbs_stack),
         }
     }

crates/extensions_ui/src/extensions_ui.rs 🔗

@@ -521,7 +521,7 @@ impl ExtensionsPage {
                     .gap_2()
                     .border_1()
                     .border_color(editor_border)
-                    .min_w(rems(384. / 16.))
+                    .min_w(rems_from_px(384.))
                     .rounded_lg()
                     .child(Icon::new(IconName::MagnifyingGlass))
                     .child(self.render_text_input(&self.query_editor, cx)),

crates/ui/src/components/button/button_like.rs 🔗

@@ -1,5 +1,5 @@
 use gpui::{relative, DefiniteLength, MouseButton};
-use gpui::{rems, transparent_black, AnyElement, AnyView, ClickEvent, Hsla, Rems};
+use gpui::{transparent_black, AnyElement, AnyView, ClickEvent, Hsla, Rems};
 use smallvec::SmallVec;
 
 use crate::prelude::*;
@@ -278,10 +278,10 @@ pub enum ButtonSize {
 impl ButtonSize {
     fn height(self) -> Rems {
         match self {
-            ButtonSize::Large => rems(32. / 16.),
-            ButtonSize::Default => rems(22. / 16.),
-            ButtonSize::Compact => rems(18. / 16.),
-            ButtonSize::None => rems(16. / 16.),
+            ButtonSize::Large => rems_from_px(32.),
+            ButtonSize::Default => rems_from_px(22.),
+            ButtonSize::Compact => rems_from_px(18.),
+            ButtonSize::None => rems_from_px(16.),
         }
     }
 }

crates/ui/src/components/icon.rs 🔗

@@ -1,4 +1,4 @@
-use gpui::{rems, svg, IntoElement, Rems};
+use gpui::{svg, IntoElement, Rems};
 use strum::EnumIter;
 
 use crate::prelude::*;
@@ -15,10 +15,10 @@ pub enum IconSize {
 impl IconSize {
     pub fn rems(self) -> Rems {
         match self {
-            IconSize::Indicator => rems(10. / 16.),
-            IconSize::XSmall => rems(12. / 16.),
-            IconSize::Small => rems(14. / 16.),
-            IconSize::Medium => rems(16. / 16.),
+            IconSize::Indicator => rems_from_px(10.),
+            IconSize::XSmall => rems_from_px(12.),
+            IconSize::Small => rems_from_px(14.),
+            IconSize::Medium => rems_from_px(16.),
         }
     }
 }

crates/ui/src/components/keybinding.rs 🔗

@@ -1,5 +1,5 @@
 use crate::{h_flex, prelude::*, Icon, IconName, IconSize};
-use gpui::{relative, rems, Action, FocusHandle, IntoElement, Keystroke};
+use gpui::{relative, Action, FocusHandle, IntoElement, Keystroke};
 
 /// The way a [`KeyBinding`] should be displayed.
 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
@@ -158,12 +158,15 @@ impl RenderOnce for Key {
             .py_0()
             .map(|this| {
                 if single_char {
-                    this.w(rems(14. / 16.)).flex().flex_none().justify_center()
+                    this.w(rems_from_px(14.))
+                        .flex()
+                        .flex_none()
+                        .justify_center()
                 } else {
                     this.px_0p5()
                 }
             })
-            .h(rems(14. / 16.))
+            .h(rems_from_px(14.))
             .text_ui()
             .line_height(relative(1.))
             .text_color(cx.theme().colors().text_muted)
@@ -184,7 +187,7 @@ pub struct KeyIcon {
 
 impl RenderOnce for KeyIcon {
     fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
-        div().w(rems(14. / 16.)).child(
+        div().w(rems_from_px(14.)).child(
             Icon::new(self.icon)
                 .size(IconSize::Small)
                 .color(Color::Muted),

crates/ui/src/components/popover_menu.rs 🔗

@@ -1,13 +1,13 @@
 use std::{cell::RefCell, rc::Rc};
 
 use gpui::{
-    div, overlay, point, prelude::FluentBuilder, px, rems, AnchorCorner, AnyElement, Bounds,
+    div, overlay, point, prelude::FluentBuilder, px, AnchorCorner, AnyElement, Bounds,
     DismissEvent, DispatchPhase, Element, ElementContext, ElementId, HitboxId, InteractiveElement,
     IntoElement, LayoutId, ManagedView, MouseDownEvent, ParentElement, Pixels, Point, View,
     VisualContext, WindowContext,
 };
 
-use crate::{Clickable, Selectable};
+use crate::prelude::*;
 
 pub trait PopoverTrigger: IntoElement + Clickable + Selectable + 'static {}
 
@@ -102,7 +102,7 @@ impl<M: ManagedView> PopoverMenu<M> {
     fn resolved_offset(&self, cx: &WindowContext) -> Point<Pixels> {
         self.offset.unwrap_or_else(|| {
             // Default offset = 4px padding + 1px border
-            let offset = rems(5. / 16.) * cx.rem_size();
+            let offset = rems_from_px(5.) * cx.rem_size();
             match self.anchor {
                 AnchorCorner::TopRight | AnchorCorner::BottomRight => point(offset, px(0.)),
                 AnchorCorner::TopLeft | AnchorCorner::BottomLeft => point(-offset, px(0.)),

crates/ui/src/components/tab.rs 🔗

@@ -1,7 +1,9 @@
-use crate::prelude::*;
+use std::cmp::Ordering;
+
 use gpui::{AnyElement, IntoElement, Stateful};
 use smallvec::SmallVec;
-use std::cmp::Ordering;
+
+use crate::{prelude::*, BASE_REM_SIZE_IN_PX};
 
 /// The position of a [`Tab`] within a list of tabs.
 #[derive(Debug, PartialEq, Eq, Clone, Copy)]
@@ -51,9 +53,9 @@ impl Tab {
         }
     }
 
-    pub const CONTAINER_HEIGHT_IN_REMS: f32 = 29. / 16.;
+    pub const CONTAINER_HEIGHT_IN_REMS: f32 = 29. / BASE_REM_SIZE_IN_PX;
 
-    const CONTENT_HEIGHT_IN_REMS: f32 = 28. / 16.;
+    const CONTENT_HEIGHT_IN_REMS: f32 = 28. / BASE_REM_SIZE_IN_PX;
 
     pub fn position(mut self, position: TabPosition) -> Self {
         self.position = position;

crates/ui/src/components/tab_bar.rs 🔗

@@ -90,15 +90,13 @@ impl ParentElement for TabBar {
 
 impl RenderOnce for TabBar {
     fn render(self, cx: &mut WindowContext) -> impl IntoElement {
-        const HEIGHT_IN_REMS: f32 = 29. / 16.;
-
         div()
             .id(self.id)
             .group("tab_bar")
             .flex()
             .flex_none()
             .w_full()
-            .h(rems(HEIGHT_IN_REMS))
+            .h(rems_from_px(29.))
             .bg(cx.theme().colors().tab_bar_background)
             .when(!self.start_children.is_empty(), |this| {
                 this.child(

crates/ui/src/prelude.rs 🔗

@@ -11,7 +11,7 @@ pub use crate::clickable::*;
 pub use crate::disableable::*;
 pub use crate::fixed::*;
 pub use crate::selectable::*;
-pub use crate::styles::{vh, vw};
+pub use crate::styles::{rems_from_px, vh, vw};
 pub use crate::visible_on_hover::*;
 pub use crate::{h_flex, v_flex};
 pub use crate::{Button, ButtonSize, ButtonStyle, IconButton, SelectableButton};

crates/ui/src/styles/typography.rs 🔗

@@ -4,6 +4,8 @@ use gpui::{
 use settings::Settings;
 use theme::{ActiveTheme, ThemeSettings};
 
+use crate::rems_from_px;
+
 #[derive(Debug, Default, Clone)]
 pub enum UiTextSize {
     /// The default size for UI text.
@@ -38,10 +40,10 @@ pub enum UiTextSize {
 impl UiTextSize {
     pub fn rems(self) -> Rems {
         match self {
-            Self::Large => rems(16. / 16.),
-            Self::Default => rems(14. / 16.),
-            Self::Small => rems(12. / 16.),
-            Self::XSmall => rems(10. / 16.),
+            Self::Large => rems_from_px(16.),
+            Self::Default => rems_from_px(14.),
+            Self::Small => rems_from_px(12.),
+            Self::XSmall => rems_from_px(10.),
         }
     }
 }

crates/ui/src/styles/units.rs 🔗

@@ -1,4 +1,18 @@
-use gpui::{Length, WindowContext};
+use gpui::{rems, Length, Rems, WindowContext};
+
+/// The base size of a rem, in pixels.
+pub(crate) const BASE_REM_SIZE_IN_PX: f32 = 16.;
+
+/// Returns a rem value derived from the provided pixel value and the base rem size (16px).
+///
+/// This can be used to compute rem values relative to pixel sizes, without
+/// needing to hard-code the rem value.
+///
+/// For instance, instead of writing `rems(0.875)` you can write `rems_from_px(14.)`
+#[inline(always)]
+pub fn rems_from_px(px: f32) -> Rems {
+    rems(px / BASE_REM_SIZE_IN_PX)
+}
 
 /// Returns a [`Length`] corresponding to the specified percentage of the viewport's width.
 ///