Scale UI elements in the editor based on the `buffer_font_size` (#11817)

Marshall Bowers created

This PR adjusts how UI elements are rendered inside of full-size editors
to scale with the configured `buffer_font_size`.

This fixes some issues where UI elements (such as the `IconButton`s used
for code action and task run indicators) would not scale as the
`buffer_font_size` was changed.

We achieve this by changing the rem size when rendering the
`EditorElement`, with a rem size that is derived from the
`buffer_font_size`.

`WindowContext` now has a new `with_rem_size` method that can be used to
render an element with a given rem size. Note that this can only be
called during `request_layout`, `prepaint`, or `paint`, similar to
`with_text_style` or `with_content_mask`.

### Before

<img width="1264" alt="Screenshot 2024-05-14 at 2 15 39 PM"
src="https://github.com/zed-industries/zed/assets/1486634/05ad7f8d-c62f-4baa-bffd-38cace7f3710">

<img width="1264" alt="Screenshot 2024-05-14 at 2 15 49 PM"
src="https://github.com/zed-industries/zed/assets/1486634/254cd11c-3723-488f-ab3d-ed653169056c">

### After

<img width="1264" alt="Screenshot 2024-05-14 at 2 13 02 PM"
src="https://github.com/zed-industries/zed/assets/1486634/c8dad309-62a4-444f-bfeb-a0009dc08c03">

<img width="1264" alt="Screenshot 2024-05-14 at 2 13 06 PM"
src="https://github.com/zed-industries/zed/assets/1486634/4d9a3a52-9656-4768-b210-840b4884e381">

Note: This diff is best viewed with whitespace changes hidden:

<img width="245" alt="Screenshot 2024-05-14 at 2 22 45 PM"
src="https://github.com/zed-industries/zed/assets/1486634/7cb9829f-9c1b-4224-95be-82182017ed90">

Release Notes:

- Changed UI elements within the editor to scale based on
`buffer_font_size` (e.g., code action indicators, task run indicators,
etc.).

Change summary

crates/editor/src/element.rs | 952 +++++++++++++++++++------------------
crates/gpui/src/window.rs    |  40 +
2 files changed, 535 insertions(+), 457 deletions(-)

Detailed changes

crates/editor/src/element.rs 🔗

@@ -58,7 +58,7 @@ use std::{
     sync::Arc,
 };
 use sum_tree::Bias;
-use theme::{ActiveTheme, PlayerColor};
+use theme::{ActiveTheme, PlayerColor, ThemeSettings};
 use ui::prelude::*;
 use ui::{h_flex, ButtonLike, ButtonStyle, ContextMenu, Tooltip};
 use util::ResultExt;
@@ -3705,6 +3705,38 @@ enum Invisible {
     Whitespace { line_offset: usize },
 }
 
+impl EditorElement {
+    /// Returns the rem size to use when rendering the [`EditorElement`].
+    ///
+    /// This allows UI elements to scale based on the `buffer_font_size`.
+    fn rem_size(&self, cx: &WindowContext) -> Option<Pixels> {
+        match self.editor.read(cx).mode {
+            EditorMode::Full => {
+                let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
+                let rem_size_scale = {
+                    // Our default UI font size is 14px on a 16px base scale.
+                    // This means the default UI font size is 0.875rems.
+                    let default_font_size_scale = 14. / ui::BASE_REM_SIZE_IN_PX;
+
+                    // We then determine the delta between a single rem and the default font
+                    // size scale.
+                    let default_font_size_delta = 1. - default_font_size_scale;
+
+                    // Finally, we add this delta to 1rem to get the scale factor that
+                    // should be used to scale up the UI.
+                    1. + default_font_size_delta
+                };
+
+                Some(buffer_font_size * rem_size_scale)
+            }
+            // We currently use single-line and auto-height editors in UI contexts,
+            // so we don't want to scale everything with the buffer font size, as it
+            // ends up looking off.
+            EditorMode::SingleLine | EditorMode::AutoHeight { .. } => None,
+        }
+    }
+}
+
 impl Element for EditorElement {
     type RequestLayoutState = ();
     type PrepaintState = EditorLayout;
@@ -3718,48 +3750,51 @@ impl Element for EditorElement {
         _: Option<&GlobalElementId>,
         cx: &mut WindowContext,
     ) -> (gpui::LayoutId, ()) {
-        self.editor.update(cx, |editor, cx| {
-            editor.set_style(self.style.clone(), cx);
-
-            let layout_id = match editor.mode {
-                EditorMode::SingleLine => {
-                    let rem_size = cx.rem_size();
-                    let mut style = Style::default();
-                    style.size.width = relative(1.).into();
-                    style.size.height = self.style.text.line_height_in_pixels(rem_size).into();
-                    cx.request_layout(style, None)
-                }
-                EditorMode::AutoHeight { max_lines } => {
-                    let editor_handle = cx.view().clone();
-                    let max_line_number_width =
-                        self.max_line_number_width(&editor.snapshot(cx), cx);
-                    cx.request_measured_layout(
-                        Style::default(),
-                        move |known_dimensions, available_space, cx| {
-                            editor_handle
-                                .update(cx, |editor, cx| {
-                                    compute_auto_height_layout(
-                                        editor,
-                                        max_lines,
-                                        max_line_number_width,
-                                        known_dimensions,
-                                        available_space.width,
-                                        cx,
-                                    )
-                                })
-                                .unwrap_or_default()
-                        },
-                    )
-                }
-                EditorMode::Full => {
-                    let mut style = Style::default();
-                    style.size.width = relative(1.).into();
-                    style.size.height = relative(1.).into();
-                    cx.request_layout(style, None)
-                }
-            };
+        let rem_size = self.rem_size(cx);
+        cx.with_rem_size(rem_size, |cx| {
+            self.editor.update(cx, |editor, cx| {
+                editor.set_style(self.style.clone(), cx);
+
+                let layout_id = match editor.mode {
+                    EditorMode::SingleLine => {
+                        let rem_size = cx.rem_size();
+                        let mut style = Style::default();
+                        style.size.width = relative(1.).into();
+                        style.size.height = self.style.text.line_height_in_pixels(rem_size).into();
+                        cx.request_layout(style, None)
+                    }
+                    EditorMode::AutoHeight { max_lines } => {
+                        let editor_handle = cx.view().clone();
+                        let max_line_number_width =
+                            self.max_line_number_width(&editor.snapshot(cx), cx);
+                        cx.request_measured_layout(
+                            Style::default(),
+                            move |known_dimensions, available_space, cx| {
+                                editor_handle
+                                    .update(cx, |editor, cx| {
+                                        compute_auto_height_layout(
+                                            editor,
+                                            max_lines,
+                                            max_line_number_width,
+                                            known_dimensions,
+                                            available_space.width,
+                                            cx,
+                                        )
+                                    })
+                                    .unwrap_or_default()
+                            },
+                        )
+                    }
+                    EditorMode::Full => {
+                        let mut style = Style::default();
+                        style.size.width = relative(1.).into();
+                        style.size.height = relative(1.).into();
+                        cx.request_layout(style, None)
+                    }
+                };
 
-            (layout_id, ())
+                (layout_id, ())
+            })
         })
     }
 
@@ -3776,478 +3811,485 @@ impl Element for EditorElement {
             ..Default::default()
         };
         cx.set_view_id(self.editor.entity_id());
-        cx.with_text_style(Some(text_style), |cx| {
-            cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
-                let mut snapshot = self.editor.update(cx, |editor, cx| editor.snapshot(cx));
-                let style = self.style.clone();
-
-                let font_id = cx.text_system().resolve_font(&style.text.font());
-                let font_size = style.text.font_size.to_pixels(cx.rem_size());
-                let line_height = style.text.line_height_in_pixels(cx.rem_size());
-                let em_width = cx
-                    .text_system()
-                    .typographic_bounds(font_id, font_size, 'm')
-                    .unwrap()
-                    .size
-                    .width;
-                let em_advance = cx
-                    .text_system()
-                    .advance(font_id, font_size, 'm')
-                    .unwrap()
-                    .width;
-
-                let gutter_dimensions = snapshot.gutter_dimensions(
-                    font_id,
-                    font_size,
-                    em_width,
-                    self.max_line_number_width(&snapshot, cx),
-                    cx,
-                );
-                let text_width = bounds.size.width - gutter_dimensions.width;
-
-                let right_margin = if snapshot.mode == EditorMode::Full {
-                    EditorElement::SCROLLBAR_WIDTH
-                } else {
-                    px(0.)
-                };
-                let overscroll = size(em_width + right_margin, px(0.));
-
-                snapshot = self.editor.update(cx, |editor, cx| {
-                    editor.last_bounds = Some(bounds);
-                    editor.gutter_dimensions = gutter_dimensions;
-                    editor.set_visible_line_count(bounds.size.height / line_height, cx);
-
-                    let editor_width =
-                        text_width - gutter_dimensions.margin - overscroll.width - em_width;
-                    let wrap_width = match editor.soft_wrap_mode(cx) {
-                        SoftWrap::None => None,
-                        SoftWrap::PreferLine => Some((MAX_LINE_LEN / 2) as f32 * em_advance),
-                        SoftWrap::EditorWidth => Some(editor_width),
-                        SoftWrap::Column(column) => {
-                            Some(editor_width.min(column as f32 * em_advance))
-                        }
-                    };
 
-                    if editor.set_wrap_width(wrap_width, cx) {
-                        editor.snapshot(cx)
-                    } else {
-                        snapshot
-                    }
-                });
+        let rem_size = self.rem_size(cx);
+        cx.with_rem_size(rem_size, |cx| {
+            cx.with_text_style(Some(text_style), |cx| {
+                cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
+                    let mut snapshot = self.editor.update(cx, |editor, cx| editor.snapshot(cx));
+                    let style = self.style.clone();
+
+                    let font_id = cx.text_system().resolve_font(&style.text.font());
+                    let font_size = style.text.font_size.to_pixels(cx.rem_size());
+                    let line_height = style.text.line_height_in_pixels(cx.rem_size());
+                    let em_width = cx
+                        .text_system()
+                        .typographic_bounds(font_id, font_size, 'm')
+                        .unwrap()
+                        .size
+                        .width;
+                    let em_advance = cx
+                        .text_system()
+                        .advance(font_id, font_size, 'm')
+                        .unwrap()
+                        .width;
 
-                let wrap_guides = self
-                    .editor
-                    .read(cx)
-                    .wrap_guides(cx)
-                    .iter()
-                    .map(|(guide, active)| (self.column_pixels(*guide, cx), *active))
-                    .collect::<SmallVec<[_; 2]>>();
-
-                let hitbox = cx.insert_hitbox(bounds, false);
-                let gutter_hitbox = cx.insert_hitbox(
-                    Bounds {
-                        origin: bounds.origin,
-                        size: size(gutter_dimensions.width, bounds.size.height),
-                    },
-                    false,
-                );
-                let text_hitbox = cx.insert_hitbox(
-                    Bounds {
-                        origin: gutter_hitbox.upper_right(),
-                        size: size(text_width, bounds.size.height),
-                    },
-                    false,
-                );
-                // Offset the content_bounds from the text_bounds by the gutter margin (which
-                // is roughly half a character wide) to make hit testing work more like how we want.
-                let content_origin =
-                    text_hitbox.origin + point(gutter_dimensions.margin, Pixels::ZERO);
-
-                let mut autoscroll_containing_element = false;
-                let mut autoscroll_horizontally = false;
-                self.editor.update(cx, |editor, cx| {
-                    autoscroll_containing_element =
-                        editor.autoscroll_requested() || editor.has_pending_selection();
-                    autoscroll_horizontally = editor.autoscroll_vertically(bounds, line_height, cx);
-                    snapshot = editor.snapshot(cx);
-                });
+                    let gutter_dimensions = snapshot.gutter_dimensions(
+                        font_id,
+                        font_size,
+                        em_width,
+                        self.max_line_number_width(&snapshot, cx),
+                        cx,
+                    );
+                    let text_width = bounds.size.width - gutter_dimensions.width;
 
-                let mut scroll_position = snapshot.scroll_position();
-                // The scroll position is a fractional point, the whole number of which represents
-                // the top of the window in terms of display rows.
-                let start_row = DisplayRow(scroll_position.y as u32);
-                let height_in_lines = bounds.size.height / line_height;
-                let max_row = snapshot.max_point().row();
-                let end_row = cmp::min(
-                    (scroll_position.y + height_in_lines).ceil() as u32,
-                    max_row.next_row().0,
-                );
-                let end_row = DisplayRow(end_row);
+                    let right_margin = if snapshot.mode == EditorMode::Full {
+                        EditorElement::SCROLLBAR_WIDTH
+                    } else {
+                        px(0.)
+                    };
+                    let overscroll = size(em_width + right_margin, px(0.));
+
+                    snapshot = self.editor.update(cx, |editor, cx| {
+                        editor.last_bounds = Some(bounds);
+                        editor.gutter_dimensions = gutter_dimensions;
+                        editor.set_visible_line_count(bounds.size.height / line_height, cx);
+
+                        let editor_width =
+                            text_width - gutter_dimensions.margin - overscroll.width - em_width;
+                        let wrap_width = match editor.soft_wrap_mode(cx) {
+                            SoftWrap::None => None,
+                            SoftWrap::PreferLine => Some((MAX_LINE_LEN / 2) as f32 * em_advance),
+                            SoftWrap::EditorWidth => Some(editor_width),
+                            SoftWrap::Column(column) => {
+                                Some(editor_width.min(column as f32 * em_advance))
+                            }
+                        };
 
-                let buffer_rows = snapshot
-                    .buffer_rows(start_row)
-                    .take((start_row..end_row).len())
-                    .collect::<Vec<_>>();
+                        if editor.set_wrap_width(wrap_width, cx) {
+                            editor.snapshot(cx)
+                        } else {
+                            snapshot
+                        }
+                    });
 
-                let start_anchor = if start_row == Default::default() {
-                    Anchor::min()
-                } else {
-                    snapshot.buffer_snapshot.anchor_before(
-                        DisplayPoint::new(start_row, 0).to_offset(&snapshot, Bias::Left),
-                    )
-                };
-                let end_anchor = if end_row > max_row {
-                    Anchor::max()
-                } else {
-                    snapshot.buffer_snapshot.anchor_before(
-                        DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right),
-                    )
-                };
+                    let wrap_guides = self
+                        .editor
+                        .read(cx)
+                        .wrap_guides(cx)
+                        .iter()
+                        .map(|(guide, active)| (self.column_pixels(*guide, cx), *active))
+                        .collect::<SmallVec<[_; 2]>>();
 
-                let highlighted_rows = self.editor.update(cx, |editor, cx| {
-                    editor.highlighted_display_rows(HashSet::default(), cx)
-                });
-                let highlighted_ranges = self.editor.read(cx).background_highlights_in_range(
-                    start_anchor..end_anchor,
-                    &snapshot.display_snapshot,
-                    cx.theme().colors(),
-                );
+                    let hitbox = cx.insert_hitbox(bounds, false);
+                    let gutter_hitbox = cx.insert_hitbox(
+                        Bounds {
+                            origin: bounds.origin,
+                            size: size(gutter_dimensions.width, bounds.size.height),
+                        },
+                        false,
+                    );
+                    let text_hitbox = cx.insert_hitbox(
+                        Bounds {
+                            origin: gutter_hitbox.upper_right(),
+                            size: size(text_width, bounds.size.height),
+                        },
+                        false,
+                    );
+                    // Offset the content_bounds from the text_bounds by the gutter margin (which
+                    // is roughly half a character wide) to make hit testing work more like how we want.
+                    let content_origin =
+                        text_hitbox.origin + point(gutter_dimensions.margin, Pixels::ZERO);
+
+                    let mut autoscroll_containing_element = false;
+                    let mut autoscroll_horizontally = false;
+                    self.editor.update(cx, |editor, cx| {
+                        autoscroll_containing_element =
+                            editor.autoscroll_requested() || editor.has_pending_selection();
+                        autoscroll_horizontally =
+                            editor.autoscroll_vertically(bounds, line_height, cx);
+                        snapshot = editor.snapshot(cx);
+                    });
 
-                let redacted_ranges = self.editor.read(cx).redacted_ranges(
-                    start_anchor..end_anchor,
-                    &snapshot.display_snapshot,
-                    cx,
-                );
+                    let mut scroll_position = snapshot.scroll_position();
+                    // The scroll position is a fractional point, the whole number of which represents
+                    // the top of the window in terms of display rows.
+                    let start_row = DisplayRow(scroll_position.y as u32);
+                    let height_in_lines = bounds.size.height / line_height;
+                    let max_row = snapshot.max_point().row();
+                    let end_row = cmp::min(
+                        (scroll_position.y + height_in_lines).ceil() as u32,
+                        max_row.next_row().0,
+                    );
+                    let end_row = DisplayRow(end_row);
 
-                let (selections, active_rows, newest_selection_head) = self.layout_selections(
-                    start_anchor,
-                    end_anchor,
-                    &snapshot,
-                    start_row,
-                    end_row,
-                    cx,
-                );
+                    let buffer_rows = snapshot
+                        .buffer_rows(start_row)
+                        .take((start_row..end_row).len())
+                        .collect::<Vec<_>>();
 
-                let (line_numbers, fold_statuses) = self.layout_line_numbers(
-                    start_row..end_row,
-                    buffer_rows.clone().into_iter(),
-                    &active_rows,
-                    newest_selection_head,
-                    &snapshot,
-                    cx,
-                );
+                    let start_anchor = if start_row == Default::default() {
+                        Anchor::min()
+                    } else {
+                        snapshot.buffer_snapshot.anchor_before(
+                            DisplayPoint::new(start_row, 0).to_offset(&snapshot, Bias::Left),
+                        )
+                    };
+                    let end_anchor = if end_row > max_row {
+                        Anchor::max()
+                    } else {
+                        snapshot.buffer_snapshot.anchor_before(
+                            DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right),
+                        )
+                    };
 
-                let display_hunks = self.layout_git_gutters(
-                    line_height,
-                    &gutter_hitbox,
-                    start_row..end_row,
-                    &snapshot,
-                    cx,
-                );
+                    let highlighted_rows = self.editor.update(cx, |editor, cx| {
+                        editor.highlighted_display_rows(HashSet::default(), cx)
+                    });
+                    let highlighted_ranges = self.editor.read(cx).background_highlights_in_range(
+                        start_anchor..end_anchor,
+                        &snapshot.display_snapshot,
+                        cx.theme().colors(),
+                    );
 
-                let mut max_visible_line_width = Pixels::ZERO;
-                let line_layouts =
-                    self.layout_lines(start_row..end_row, &line_numbers, &snapshot, cx);
-                for line_with_invisibles in &line_layouts {
-                    if line_with_invisibles.line.width > max_visible_line_width {
-                        max_visible_line_width = line_with_invisibles.line.width;
-                    }
-                }
+                    let redacted_ranges = self.editor.read(cx).redacted_ranges(
+                        start_anchor..end_anchor,
+                        &snapshot.display_snapshot,
+                        cx,
+                    );
 
-                let longest_line_width = layout_line(snapshot.longest_row(), &snapshot, &style, cx)
-                    .unwrap()
-                    .width;
-                let mut scroll_width =
-                    longest_line_width.max(max_visible_line_width) + overscroll.width;
+                    let (selections, active_rows, newest_selection_head) = self.layout_selections(
+                        start_anchor,
+                        end_anchor,
+                        &snapshot,
+                        start_row,
+                        end_row,
+                        cx,
+                    );
 
-                let mut blocks = cx.with_element_namespace("blocks", |cx| {
-                    self.build_blocks(
+                    let (line_numbers, fold_statuses) = self.layout_line_numbers(
                         start_row..end_row,
+                        buffer_rows.clone().into_iter(),
+                        &active_rows,
+                        newest_selection_head,
                         &snapshot,
-                        &hitbox,
-                        &text_hitbox,
-                        &mut scroll_width,
-                        &gutter_dimensions,
-                        em_width,
-                        gutter_dimensions.width + gutter_dimensions.margin,
-                        line_height,
-                        &line_layouts,
                         cx,
-                    )
-                });
+                    );
 
-                let scroll_pixel_position = point(
-                    scroll_position.x * em_width,
-                    scroll_position.y * line_height,
-                );
+                    let display_hunks = self.layout_git_gutters(
+                        line_height,
+                        &gutter_hitbox,
+                        start_row..end_row,
+                        &snapshot,
+                        cx,
+                    );
 
-                let mut inline_blame = None;
-                if let Some(newest_selection_head) = newest_selection_head {
-                    let display_row = newest_selection_head.row();
-                    if (start_row..end_row).contains(&display_row) {
-                        let line_layout = &line_layouts[display_row.minus(start_row) as usize];
-                        inline_blame = self.layout_inline_blame(
-                            display_row,
-                            &snapshot.display_snapshot,
-                            line_layout,
-                            em_width,
-                            content_origin,
-                            scroll_pixel_position,
-                            line_height,
-                            cx,
-                        );
+                    let mut max_visible_line_width = Pixels::ZERO;
+                    let line_layouts =
+                        self.layout_lines(start_row..end_row, &line_numbers, &snapshot, cx);
+                    for line_with_invisibles in &line_layouts {
+                        if line_with_invisibles.line.width > max_visible_line_width {
+                            max_visible_line_width = line_with_invisibles.line.width;
+                        }
                     }
-                }
-
-                let blamed_display_rows = self.layout_blame_entries(
-                    buffer_rows.into_iter(),
-                    em_width,
-                    scroll_position,
-                    line_height,
-                    &gutter_hitbox,
-                    gutter_dimensions.git_blame_entries_width,
-                    cx,
-                );
 
-                let scroll_max = point(
-                    ((scroll_width - text_hitbox.size.width) / em_width).max(0.0),
-                    max_row.as_f32(),
-                );
-
-                self.editor.update(cx, |editor, cx| {
-                    let clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x);
+                    let longest_line_width =
+                        layout_line(snapshot.longest_row(), &snapshot, &style, cx)
+                            .unwrap()
+                            .width;
+                    let mut scroll_width =
+                        longest_line_width.max(max_visible_line_width) + overscroll.width;
 
-                    let autoscrolled = if autoscroll_horizontally {
-                        editor.autoscroll_horizontally(
-                            start_row,
-                            text_hitbox.size.width,
-                            scroll_width,
+                    let mut blocks = cx.with_element_namespace("blocks", |cx| {
+                        self.build_blocks(
+                            start_row..end_row,
+                            &snapshot,
+                            &hitbox,
+                            &text_hitbox,
+                            &mut scroll_width,
+                            &gutter_dimensions,
                             em_width,
+                            gutter_dimensions.width + gutter_dimensions.margin,
+                            line_height,
                             &line_layouts,
                             cx,
                         )
-                    } else {
-                        false
-                    };
+                    });
 
-                    if clamped || autoscrolled {
-                        snapshot = editor.snapshot(cx);
-                        scroll_position = snapshot.scroll_position();
+                    let scroll_pixel_position = point(
+                        scroll_position.x * em_width,
+                        scroll_position.y * line_height,
+                    );
+
+                    let mut inline_blame = None;
+                    if let Some(newest_selection_head) = newest_selection_head {
+                        let display_row = newest_selection_head.row();
+                        if (start_row..end_row).contains(&display_row) {
+                            let line_layout = &line_layouts[display_row.minus(start_row) as usize];
+                            inline_blame = self.layout_inline_blame(
+                                display_row,
+                                &snapshot.display_snapshot,
+                                line_layout,
+                                em_width,
+                                content_origin,
+                                scroll_pixel_position,
+                                line_height,
+                                cx,
+                            );
+                        }
                     }
-                });
 
-                cx.with_element_namespace("blocks", |cx| {
-                    self.layout_blocks(
-                        &mut blocks,
-                        &hitbox,
+                    let blamed_display_rows = self.layout_blame_entries(
+                        buffer_rows.into_iter(),
+                        em_width,
+                        scroll_position,
                         line_height,
-                        scroll_pixel_position,
+                        &gutter_hitbox,
+                        gutter_dimensions.git_blame_entries_width,
                         cx,
                     );
-                });
 
-                let cursors = self.collect_cursors(&snapshot, cx);
-                let visible_row_range = start_row..end_row;
-                let non_visible_cursors = cursors
-                    .iter()
-                    .any(move |c| !visible_row_range.contains(&c.0.row()));
+                    let scroll_max = point(
+                        ((scroll_width - text_hitbox.size.width) / em_width).max(0.0),
+                        max_row.as_f32(),
+                    );
 
-                let visible_cursors = self.layout_visible_cursors(
-                    &snapshot,
-                    &selections,
-                    start_row..end_row,
-                    &line_layouts,
-                    &text_hitbox,
-                    content_origin,
-                    scroll_position,
-                    scroll_pixel_position,
-                    line_height,
-                    em_width,
-                    autoscroll_containing_element,
-                    cx,
-                );
+                    self.editor.update(cx, |editor, cx| {
+                        let clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x);
 
-                let scrollbar_layout = self.layout_scrollbar(
-                    &snapshot,
-                    bounds,
-                    scroll_position,
-                    height_in_lines,
-                    non_visible_cursors,
-                    cx,
-                );
+                        let autoscrolled = if autoscroll_horizontally {
+                            editor.autoscroll_horizontally(
+                                start_row,
+                                text_hitbox.size.width,
+                                scroll_width,
+                                em_width,
+                                &line_layouts,
+                                cx,
+                            )
+                        } else {
+                            false
+                        };
 
-                let folds = cx.with_element_namespace("folds", |cx| {
-                    self.layout_folds(
+                        if clamped || autoscrolled {
+                            snapshot = editor.snapshot(cx);
+                            scroll_position = snapshot.scroll_position();
+                        }
+                    });
+
+                    cx.with_element_namespace("blocks", |cx| {
+                        self.layout_blocks(
+                            &mut blocks,
+                            &hitbox,
+                            line_height,
+                            scroll_pixel_position,
+                            cx,
+                        );
+                    });
+
+                    let cursors = self.collect_cursors(&snapshot, cx);
+                    let visible_row_range = start_row..end_row;
+                    let non_visible_cursors = cursors
+                        .iter()
+                        .any(move |c| !visible_row_range.contains(&c.0.row()));
+
+                    let visible_cursors = self.layout_visible_cursors(
                         &snapshot,
-                        content_origin,
-                        start_anchor..end_anchor,
+                        &selections,
                         start_row..end_row,
+                        &line_layouts,
+                        &text_hitbox,
+                        content_origin,
+                        scroll_position,
                         scroll_pixel_position,
                         line_height,
-                        &line_layouts,
+                        em_width,
+                        autoscroll_containing_element,
                         cx,
-                    )
-                });
+                    );
 
-                let gutter_settings = EditorSettings::get_global(cx).gutter;
+                    let scrollbar_layout = self.layout_scrollbar(
+                        &snapshot,
+                        bounds,
+                        scroll_position,
+                        height_in_lines,
+                        non_visible_cursors,
+                        cx,
+                    );
 
-                let mut context_menu_visible = false;
-                let mut code_actions_indicator = None;
-                if let Some(newest_selection_head) = newest_selection_head {
-                    if (start_row..end_row).contains(&newest_selection_head.row()) {
-                        context_menu_visible = self.layout_context_menu(
-                            line_height,
-                            &hitbox,
-                            &text_hitbox,
+                    let folds = cx.with_element_namespace("folds", |cx| {
+                        self.layout_folds(
+                            &snapshot,
                             content_origin,
-                            start_row,
+                            start_anchor..end_anchor,
+                            start_row..end_row,
                             scroll_pixel_position,
+                            line_height,
                             &line_layouts,
-                            newest_selection_head,
-                            gutter_dimensions.width - gutter_dimensions.left_padding,
                             cx,
-                        );
-                        if gutter_settings.code_actions {
-                            let newest_selection_point =
-                                newest_selection_head.to_point(&snapshot.display_snapshot);
-                            let buffer = snapshot
-                                .buffer_snapshot
-                                .buffer_line_for_row(MultiBufferRow(newest_selection_point.row));
-                            if let Some((buffer, range)) = buffer {
-                                let buffer_id = buffer.remote_id();
-                                let row = range.start.row;
-                                let has_test_indicator =
-                                    self.editor.read(cx).tasks.contains_key(&(buffer_id, row));
-
-                                if !has_test_indicator {
-                                    code_actions_indicator = self.layout_code_actions_indicator(
-                                        line_height,
-                                        newest_selection_head,
-                                        scroll_pixel_position,
-                                        &gutter_dimensions,
-                                        &gutter_hitbox,
-                                        cx,
-                                    );
+                        )
+                    });
+
+                    let gutter_settings = EditorSettings::get_global(cx).gutter;
+
+                    let mut context_menu_visible = false;
+                    let mut code_actions_indicator = None;
+                    if let Some(newest_selection_head) = newest_selection_head {
+                        if (start_row..end_row).contains(&newest_selection_head.row()) {
+                            context_menu_visible = self.layout_context_menu(
+                                line_height,
+                                &hitbox,
+                                &text_hitbox,
+                                content_origin,
+                                start_row,
+                                scroll_pixel_position,
+                                &line_layouts,
+                                newest_selection_head,
+                                gutter_dimensions.width - gutter_dimensions.left_padding,
+                                cx,
+                            );
+                            if gutter_settings.code_actions {
+                                let newest_selection_point =
+                                    newest_selection_head.to_point(&snapshot.display_snapshot);
+                                let buffer = snapshot.buffer_snapshot.buffer_line_for_row(
+                                    MultiBufferRow(newest_selection_point.row),
+                                );
+                                if let Some((buffer, range)) = buffer {
+                                    let buffer_id = buffer.remote_id();
+                                    let row = range.start.row;
+                                    let has_test_indicator =
+                                        self.editor.read(cx).tasks.contains_key(&(buffer_id, row));
+
+                                    if !has_test_indicator {
+                                        code_actions_indicator = self
+                                            .layout_code_actions_indicator(
+                                                line_height,
+                                                newest_selection_head,
+                                                scroll_pixel_position,
+                                                &gutter_dimensions,
+                                                &gutter_hitbox,
+                                                cx,
+                                            );
+                                    }
                                 }
                             }
                         }
                     }
-                }
 
-                let test_indicators = self.layout_run_indicators(
-                    line_height,
-                    scroll_pixel_position,
-                    &gutter_dimensions,
-                    &gutter_hitbox,
-                    &snapshot,
-                    cx,
-                );
-
-                if !context_menu_visible && !cx.has_active_drag() {
-                    self.layout_hover_popovers(
-                        &snapshot,
-                        &hitbox,
-                        &text_hitbox,
-                        start_row..end_row,
-                        content_origin,
-                        scroll_pixel_position,
-                        &line_layouts,
+                    let test_indicators = self.layout_run_indicators(
                         line_height,
-                        em_width,
+                        scroll_pixel_position,
+                        &gutter_dimensions,
+                        &gutter_hitbox,
+                        &snapshot,
                         cx,
                     );
-                }
-
-                let mouse_context_menu = self.layout_mouse_context_menu(cx);
 
-                let fold_indicators = if gutter_settings.folds {
-                    cx.with_element_namespace("gutter_fold_indicators", |cx| {
-                        self.layout_gutter_fold_indicators(
-                            fold_statuses,
-                            line_height,
-                            &gutter_dimensions,
-                            gutter_settings,
+                    if !context_menu_visible && !cx.has_active_drag() {
+                        self.layout_hover_popovers(
+                            &snapshot,
+                            &hitbox,
+                            &text_hitbox,
+                            start_row..end_row,
+                            content_origin,
                             scroll_pixel_position,
-                            &gutter_hitbox,
+                            &line_layouts,
+                            line_height,
+                            em_width,
                             cx,
-                        )
-                    })
-                } else {
-                    Vec::new()
-                };
+                        );
+                    }
 
-                let invisible_symbol_font_size = font_size / 2.;
-                let tab_invisible = cx
-                    .text_system()
-                    .shape_line(
-                        "→".into(),
-                        invisible_symbol_font_size,
-                        &[TextRun {
-                            len: "→".len(),
-                            font: self.style.text.font(),
-                            color: cx.theme().colors().editor_invisible,
-                            background_color: None,
-                            underline: None,
-                            strikethrough: None,
-                        }],
-                    )
-                    .unwrap();
-                let space_invisible = cx
-                    .text_system()
-                    .shape_line(
-                        "•".into(),
-                        invisible_symbol_font_size,
-                        &[TextRun {
-                            len: "•".len(),
-                            font: self.style.text.font(),
-                            color: cx.theme().colors().editor_invisible,
-                            background_color: None,
-                            underline: None,
-                            strikethrough: None,
-                        }],
-                    )
-                    .unwrap();
+                    let mouse_context_menu = self.layout_mouse_context_menu(cx);
 
-                EditorLayout {
-                    mode: snapshot.mode,
-                    position_map: Arc::new(PositionMap {
-                        size: bounds.size,
-                        scroll_pixel_position,
-                        scroll_max,
-                        line_layouts,
-                        line_height,
-                        em_width,
-                        em_advance,
-                        snapshot,
-                    }),
-                    visible_display_row_range: start_row..end_row,
-                    wrap_guides,
-                    hitbox,
-                    text_hitbox,
-                    gutter_hitbox,
-                    gutter_dimensions,
-                    content_origin,
-                    scrollbar_layout,
-                    active_rows,
-                    highlighted_rows,
-                    highlighted_ranges,
-                    redacted_ranges,
-                    line_numbers,
-                    display_hunks,
-                    blamed_display_rows,
-                    inline_blame,
-                    folds,
-                    blocks,
-                    cursors,
-                    visible_cursors,
-                    selections,
-                    mouse_context_menu,
-                    test_indicators,
-                    code_actions_indicator,
-                    fold_indicators,
-                    tab_invisible,
-                    space_invisible,
-                }
+                    let fold_indicators = if gutter_settings.folds {
+                        cx.with_element_namespace("gutter_fold_indicators", |cx| {
+                            self.layout_gutter_fold_indicators(
+                                fold_statuses,
+                                line_height,
+                                &gutter_dimensions,
+                                gutter_settings,
+                                scroll_pixel_position,
+                                &gutter_hitbox,
+                                cx,
+                            )
+                        })
+                    } else {
+                        Vec::new()
+                    };
+
+                    let invisible_symbol_font_size = font_size / 2.;
+                    let tab_invisible = cx
+                        .text_system()
+                        .shape_line(
+                            "→".into(),
+                            invisible_symbol_font_size,
+                            &[TextRun {
+                                len: "→".len(),
+                                font: self.style.text.font(),
+                                color: cx.theme().colors().editor_invisible,
+                                background_color: None,
+                                underline: None,
+                                strikethrough: None,
+                            }],
+                        )
+                        .unwrap();
+                    let space_invisible = cx
+                        .text_system()
+                        .shape_line(
+                            "•".into(),
+                            invisible_symbol_font_size,
+                            &[TextRun {
+                                len: "•".len(),
+                                font: self.style.text.font(),
+                                color: cx.theme().colors().editor_invisible,
+                                background_color: None,
+                                underline: None,
+                                strikethrough: None,
+                            }],
+                        )
+                        .unwrap();
+
+                    EditorLayout {
+                        mode: snapshot.mode,
+                        position_map: Arc::new(PositionMap {
+                            size: bounds.size,
+                            scroll_pixel_position,
+                            scroll_max,
+                            line_layouts,
+                            line_height,
+                            em_width,
+                            em_advance,
+                            snapshot,
+                        }),
+                        visible_display_row_range: start_row..end_row,
+                        wrap_guides,
+                        hitbox,
+                        text_hitbox,
+                        gutter_hitbox,
+                        gutter_dimensions,
+                        content_origin,
+                        scrollbar_layout,
+                        active_rows,
+                        highlighted_rows,
+                        highlighted_ranges,
+                        redacted_ranges,
+                        line_numbers,
+                        display_hunks,
+                        blamed_display_rows,
+                        inline_blame,
+                        folds,
+                        blocks,
+                        cursors,
+                        visible_cursors,
+                        selections,
+                        mouse_context_menu,
+                        test_indicators,
+                        code_actions_indicator,
+                        fold_indicators,
+                        tab_invisible,
+                        space_invisible,
+                    }
+                })
             })
         })
     }

crates/gpui/src/window.rs 🔗

@@ -490,7 +490,15 @@ pub struct Window {
     display_id: DisplayId,
     sprite_atlas: Arc<dyn PlatformAtlas>,
     text_system: Arc<WindowTextSystem>,
-    pub(crate) rem_size: Pixels,
+    rem_size: Pixels,
+    /// An override value 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>,
     pub(crate) viewport_size: Size<Pixels>,
     layout_engine: Option<TaffyLayoutEngine>,
     pub(crate) root_view: Option<AnyView>,
@@ -763,6 +771,7 @@ impl Window {
             sprite_atlas,
             text_system,
             rem_size: px(16.),
+            rem_size_override: None,
             viewport_size: content_size,
             layout_engine: Some(TaffyLayoutEngine::new()),
             root_view: None,
@@ -1202,7 +1211,9 @@ impl<'a> WindowContext<'a> {
     /// The size of an em for the base font of the application. Adjusting this value allows the
     /// UI to scale, just like zooming a web page.
     pub fn rem_size(&self) -> Pixels {
-        self.window.rem_size
+        self.window
+            .rem_size_override
+            .unwrap_or(self.window.rem_size)
     }
 
     /// Sets the size of an em for the base font of the application. Adjusting this value allows the
@@ -1211,6 +1222,31 @@ impl<'a> WindowContext<'a> {
         self.window.rem_size = rem_size.into();
     }
 
+    /// Executes the provided function with the specified rem size.
+    ///
+    /// This method must only be called as part of element drawing.
+    pub fn with_rem_size<F, R>(&mut self, rem_size: Option<impl Into<Pixels>>, f: F) -> R
+    where
+        F: FnOnce(&mut Self) -> R,
+    {
+        debug_assert!(
+            matches!(
+                self.window.draw_phase,
+                DrawPhase::Prepaint | DrawPhase::Paint
+            ),
+            "this method can only be called during request_layout, prepaint, or paint"
+        );
+
+        if let Some(rem_size) = rem_size {
+            self.window.rem_size_override = Some(rem_size.into());
+            let result = f(self);
+            self.window.rem_size_override.take();
+            result
+        } else {
+            f(self)
+        }
+    }
+
     /// The line height associated with the current text style.
     pub fn line_height(&self) -> Pixels {
         let rem_size = self.rem_size();