Skip to content

Commit 8aa3bcb

Browse files
committed
input: Scale gutter markers with the editor font
Make new gutter presentation fields private and configure the renderer, size and gap through InputEditorStyle builders and readers. Base uses application-supplied metrics for gutter reservation and marker centering. Project matching icon and gutter dimensions from the input's effective font after size defaults and caller overrides. Cover rem scaling, editor-specific font changes, three- and four-digit line numbers and horizontal scrolling. Update both documentation locales. AI-assisted implementation.
1 parent d7d678e commit 8aa3bcb

8 files changed

Lines changed: 347 additions & 119 deletions

File tree

‎crates/base/src/input/base/element.rs‎

Lines changed: 141 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,6 @@ pub(super) const RIGHT_MARGIN: Pixels = px(10.);
5151
pub(super) const LINE_NUMBER_RIGHT_MARGIN: Pixels = px(6.);
5252
const FOLD_ICON_WIDTH: Pixels = px(14.);
5353
const FOLD_ICON_HITBOX_WIDTH: Pixels = px(18.);
54-
/// Width and height of a line decoration gutter marker.
55-
const GUTTER_MARKER_SIZE: Pixels = px(12.);
56-
/// Space between a gutter marker and the line numbers.
57-
const GUTTER_MARKER_GAP: Pixels = px(4.);
5854
const MAX_HIGHLIGHT_LINE_LENGTH: usize = 10_000;
5955
const MIN_LINE_NUMBER_DIGITS: usize = 3;
6056
const MAX_LINE_NUMBER_DIGITS: usize = 7;
@@ -1127,7 +1123,9 @@ impl<M: InputModeKind> TextElement<M> {
11271123
None,
11281124
);
11291125

1130-
Self::gutter_marker_width(state) + empty_line_number.width + LINE_NUMBER_RIGHT_MARGIN
1126+
Self::gutter_marker_width(state, window)
1127+
+ empty_line_number.width
1128+
+ LINE_NUMBER_RIGHT_MARGIN
11311129
} else if state.is_code_editor() {
11321130
LINE_NUMBER_RIGHT_MARGIN
11331131
} else {
@@ -1147,12 +1145,24 @@ impl<M: InputModeKind> TextElement<M> {
11471145
/// The slot is reserved while line numbers are shown, a marker renderer is set
11481146
/// and a line decoration collection has a provider, so the gutter keeps its
11491147
/// width as marked rows scroll in and out of view.
1150-
fn gutter_marker_width(state: &InputBaseState<M>) -> Pixels {
1148+
fn gutter_marker_width(state: &InputBaseState<M>, window: &Window) -> Pixels {
11511149
if state.mode.line_number()
1152-
&& state.editor_style.gutter_marker_renderer.is_some()
1150+
&& state.editor_style.gutter_marker_renderer().is_some()
11531151
&& state.extras.has_line_decorations()
11541152
{
1155-
GUTTER_MARKER_SIZE + GUTTER_MARKER_GAP
1153+
let size = state
1154+
.editor_style
1155+
.gutter_marker_size()
1156+
.to_pixels(window.rem_size());
1157+
if size > Pixels::ZERO {
1158+
size + state
1159+
.editor_style
1160+
.gutter_marker_gap()
1161+
.to_pixels(window.rem_size())
1162+
.max(px(0.))
1163+
} else {
1164+
px(0.)
1165+
}
11561166
} else {
11571167
px(0.)
11581168
}
@@ -1464,14 +1474,21 @@ impl<M: InputModeKind> TextElement<M> {
14641474
) else {
14651475
return LineDecorationLayout::default();
14661476
};
1467-
let (decorations, renderer) = {
1477+
let (decorations, renderer, marker_size) = {
14681478
let state = self.state.read(cx);
14691479
let renderer = state
14701480
.mode
14711481
.line_number()
1472-
.then(|| state.editor_style.gutter_marker_renderer.clone())
1482+
.then(|| state.editor_style.gutter_marker_renderer().cloned())
14731483
.flatten();
1474-
(state.extras.line_decorations(first..last + 1, cx), renderer)
1484+
(
1485+
state.extras.line_decorations(first..last + 1, cx),
1486+
renderer,
1487+
state
1488+
.editor_style
1489+
.gutter_marker_size()
1490+
.to_pixels(window.rem_size()),
1491+
)
14751492
};
14761493
if decorations.is_empty() {
14771494
return LineDecorationLayout::default();
@@ -1494,18 +1511,15 @@ impl<M: InputModeKind> TextElement<M> {
14941511
if let Some(color) = decoration.background() {
14951512
layout.backgrounds.push((top, height, color));
14961513
}
1497-
if let (Some(marker), Some(render)) = (decoration.marker(), renderer.as_ref()) {
1514+
if let (Some(marker), Some(render)) = (decoration.marker(), renderer.as_ref())
1515+
&& marker_size > Pixels::ZERO
1516+
{
14981517
let origin = point(
14991518
origin_x,
1500-
bounds.origin.y + top + (line_height - GUTTER_MARKER_SIZE).half(),
1519+
bounds.origin.y + top + (line_height - marker_size).half(),
15011520
);
15021521
let mut element = render(marker);
1503-
element.prepaint_as_root(
1504-
origin,
1505-
size(GUTTER_MARKER_SIZE, GUTTER_MARKER_SIZE).into(),
1506-
window,
1507-
cx,
1508-
);
1522+
element.prepaint_as_root(origin, size(marker_size, marker_size).into(), window, cx);
15091523
layout.markers.push(element);
15101524
}
15111525
}
@@ -2740,7 +2754,7 @@ impl<M: InputModeKind> Element for TextElement<M> {
27402754
wrap_width,
27412755
wrapping_indent,
27422756
line_number_width,
2743-
gutter_marker_width: Self::gutter_marker_width(state),
2757+
gutter_marker_width: Self::gutter_marker_width(state, window),
27442758
ghost_lines: None,
27452759
space_width,
27462760
lines: Rc::new(vec![]),
@@ -3812,13 +3826,15 @@ mod tests {
38123826
let built = Rc::new(std::cell::Cell::new(0));
38133827
let counter = built.clone();
38143828
editor.update(cx, |state, _| {
3815-
state.set_editor_style(crate::input::InputEditorStyle {
3816-
gutter_marker_renderer: Some(Rc::new(move |_| {
3817-
counter.set(counter.get() + 1);
3818-
div().into_any_element()
3819-
})),
3820-
..Default::default()
3821-
});
3829+
state.set_editor_style(
3830+
crate::input::InputEditorStyle::default()
3831+
.with_gutter_marker_renderer(Some(Rc::new(move |_| {
3832+
counter.set(counter.get() + 1);
3833+
div().into_any_element()
3834+
})))
3835+
.with_gutter_marker_size(gpui::rems(0.75))
3836+
.with_gutter_marker_gap(gpui::rems(0.25)),
3837+
);
38223838
});
38233839
built
38243840
}
@@ -3831,18 +3847,20 @@ mod tests {
38313847
let laid_out = Rc::new(std::cell::RefCell::new(Vec::new()));
38323848
let record = laid_out.clone();
38333849
editor.update(cx, |state, _| {
3834-
state.set_editor_style(crate::input::InputEditorStyle {
3835-
gutter_marker_renderer: Some(Rc::new(move |_| {
3836-
let record = record.clone();
3837-
gpui::canvas(
3838-
move |bounds, _, _| record.borrow_mut().push(bounds),
3839-
|_, _, _, _| {},
3840-
)
3841-
.size_full()
3842-
.into_any_element()
3843-
})),
3844-
..Default::default()
3845-
});
3850+
state.set_editor_style(
3851+
crate::input::InputEditorStyle::default()
3852+
.with_gutter_marker_renderer(Some(Rc::new(move |_| {
3853+
let record = record.clone();
3854+
gpui::canvas(
3855+
move |bounds, _, _| record.borrow_mut().push(bounds),
3856+
|_, _, _, _| {},
3857+
)
3858+
.size(gpui::rems(0.75))
3859+
.into_any_element()
3860+
})))
3861+
.with_gutter_marker_size(gpui::rems(0.75))
3862+
.with_gutter_marker_gap(gpui::rems(0.25)),
3863+
);
38463864
});
38473865
laid_out
38483866
}
@@ -3959,75 +3977,95 @@ mod tests {
39593977
#[gpui::test]
39603978
fn gutter_markers_have_a_slot_left_of_the_line_numbers(cx: &mut TestAppContext) {
39613979
// Three-digit numbers fill the narrowest column; longer ones widen it.
3962-
for (lines, widest) in [(150, 99), (1_200, 999)] {
3963-
let text = format!("{}\n{}", "wide ".repeat(80), "x\n".repeat(lines));
3964-
let (editor, window) = decoration_editor(cx, &text, false);
3965-
let mut cx = VisualTestContext::from_window(window.into(), cx);
3966-
cx.update(|window, cx| {
3967-
let laid_out = record_markers(&editor, cx);
3968-
let padding = px(6.);
3969-
editor.update(cx, |state, _| {
3970-
state.set_editor_paddings(Edges {
3971-
left: padding,
3972-
..Default::default()
3980+
for rem_size in [16., 24., 32.] {
3981+
for (lines, widest) in [(150, 99), (1_200, 999)] {
3982+
let text = format!("{}\n{}", "wide ".repeat(80), "x\n".repeat(lines));
3983+
let (editor, window) = decoration_editor(cx, &text, false);
3984+
let mut cx = VisualTestContext::from_window(window.into(), cx);
3985+
cx.update(|window, cx| {
3986+
window.set_rem_size(px(rem_size));
3987+
let laid_out = record_markers(&editor, cx);
3988+
let padding = px(6.);
3989+
editor.update(cx, |state, _| {
3990+
state.set_editor_paddings(Edges {
3991+
left: padding,
3992+
..Default::default()
3993+
});
39733994
});
3974-
});
3975-
window.draw(cx).clear(cx);
3976-
let line_number_width = |cx: &App| {
3977-
editor
3978-
.read(cx)
3979-
.last_layout
3980-
.as_ref()
3981-
.unwrap()
3982-
.line_number_width
3983-
};
3984-
let plain = line_number_width(cx);
3995+
window.draw(cx).clear(cx);
3996+
let line_number_width = |cx: &App| {
3997+
editor
3998+
.read(cx)
3999+
.last_layout
4000+
.as_ref()
4001+
.unwrap()
4002+
.line_number_width
4003+
};
4004+
let plain = line_number_width(cx);
39854005

3986-
let collection = editor.update(cx, |state, cx| {
3987-
let height = state.last_layout.as_ref().unwrap().line_height;
3988-
state.set_scroll_offset(point(px(0.), -height * (lines - 4) as f32), cx);
3989-
state.create_line_decorations_collection(Bands::new(true), cx)
3990-
});
3991-
window.draw(cx).clear(cx);
3992-
window.draw(cx).clear(cx);
3993-
assert!(editor.read(cx).visible_row_range().unwrap().start > widest);
3994-
3995-
// The numbers keep their right edge, so the growth is the slot.
3996-
let slot = line_number_width(cx) - plain;
3997-
let input_bounds = editor.read(cx).input_bounds;
3998-
let markers_x = || {
3999-
let markers = laid_out.borrow();
4000-
assert!(!markers.is_empty());
4001-
for marker in markers.iter() {
4002-
assert!(
4003-
marker.left() >= input_bounds.left() - padding
4004-
&& marker.right() <= input_bounds.left() + slot,
4005-
"{marker:?} is not in the slot left of {lines} line numbers"
4006-
);
4007-
}
4008-
markers
4006+
let collection = editor.update(cx, |state, cx| {
4007+
let height = state.last_layout.as_ref().unwrap().line_height;
4008+
state.set_scroll_offset(point(px(0.), -height * (lines - 4) as f32), cx);
4009+
state.create_line_decorations_collection(Bands::new(true), cx)
4010+
});
4011+
window.draw(cx).clear(cx);
4012+
laid_out.borrow_mut().clear();
4013+
window.draw(cx).clear(cx);
4014+
assert!(editor.read(cx).visible_row_range().unwrap().start > widest);
4015+
4016+
// The numbers keep their right edge, so the growth is the slot.
4017+
let slot = line_number_width(cx) - plain;
4018+
let input_bounds = editor.read(cx).input_bounds;
4019+
let state = editor.read(cx);
4020+
let layout = state.last_layout.as_ref().unwrap();
4021+
let row_centers = layout
4022+
.visible_buffer_lines
40094023
.iter()
4010-
.map(|marker| marker.left())
4011-
.collect::<Vec<_>>()
4012-
};
4013-
let unscrolled = markers_x();
4024+
.map(|&row| {
4025+
state.row_bounds(row).unwrap().top() + layout.line_height.half()
4026+
})
4027+
.collect::<Vec<_>>();
4028+
let markers_x = || {
4029+
let markers = laid_out.borrow();
4030+
assert!(!markers.is_empty());
4031+
for marker in markers.iter() {
4032+
assert!(
4033+
marker.left() >= input_bounds.left() - padding
4034+
&& marker.right() <= input_bounds.left() + slot,
4035+
"{marker:?} is not in the slot left of {lines} line numbers"
4036+
);
4037+
assert!(
4038+
row_centers
4039+
.iter()
4040+
.any(|&center| (marker.center().y - center).abs() < px(0.01)),
4041+
"{marker:?} is not centered on a row at rem {rem_size}"
4042+
);
4043+
}
4044+
markers
4045+
.iter()
4046+
.map(|marker| marker.left())
4047+
.collect::<Vec<_>>()
4048+
};
4049+
let unscrolled = markers_x();
40144050

4015-
// Scrolling horizontally moves the text, not the gutter.
4016-
laid_out.borrow_mut().clear();
4017-
editor.update(cx, |state, cx| {
4018-
let offset = state.scroll_handle.offset();
4019-
state.set_scroll_offset(point(px(-200.), offset.y), cx);
4051+
// Scrolling horizontally moves the text, not the gutter.
4052+
laid_out.borrow_mut().clear();
4053+
editor.update(cx, |state, cx| {
4054+
let offset = state.scroll_handle.offset();
4055+
state.set_scroll_offset(point(px(-200.), offset.y), cx);
4056+
});
4057+
window.draw(cx).clear(cx);
4058+
laid_out.borrow_mut().clear();
4059+
window.draw(cx).clear(cx);
4060+
assert!(editor.read(cx).scroll_handle.offset().x < px(0.));
4061+
assert_eq!(markers_x()[..unscrolled.len()], unscrolled[..]);
4062+
4063+
// Without a provider there is no slot.
4064+
collection.clear(cx);
4065+
window.draw(cx).clear(cx);
4066+
assert_eq!(line_number_width(cx), plain);
40204067
});
4021-
window.draw(cx).clear(cx);
4022-
window.draw(cx).clear(cx);
4023-
assert!(editor.read(cx).scroll_handle.offset().x < px(0.));
4024-
assert_eq!(markers_x()[..unscrolled.len()], unscrolled[..]);
4025-
4026-
// Without a provider there is no slot.
4027-
collection.clear(cx);
4028-
window.draw(cx).clear(cx);
4029-
assert_eq!(line_number_width(cx), plain);
4030-
});
4068+
}
40314069
}
40324070
}
40334071

0 commit comments

Comments
 (0)