History

editor: Make `TextAlign::Center` and `TextAlign::Right` work (#45417)

Closes https://github.com/zed-industries/zed/issues/43208 This PR essentially unblocks the editable number field. The function that shapes editor lines was hard-coding text alignment to the left, meaning that whatever different alignment we'd pass through `EditorStyles`would be ignored. To solve this, I just added a text align and align width fields to the line paint function and updated all call sites keeping the default configuration. Had to also add an `alignment_offset()` helper to make sure the cursor positioning, the selection background element, and the click-to-focus functionality were kept in-sync with the non-left aligned editor. Then... the big star of the show here is being able to add the `mode` method to the number field, which uses `TextAlign::Center`, thus making it work as we designed it to work. https://github.com/user-attachments/assets/3539c976-d7bf-4d94-8188-a14328f94fbf Next up, is turning the number filed to edit mode where applicable. Release Notes: - Fixed a bug where different text alignment configurations (i.e., center and right-aligned) wouldn't take effect in editors.

Danilo Leal committed 10 months ago

Commit 0facdfa1 parent 58461375 files · +164 −50Browse files

Unified · Split

5 files
  1. crates/editor/src/element.rs +86 −20
  2. crates/gpui/examples/input.rs +9 −2
  3. crates/gpui/src/text_system/line.rs +8 −4
  4. crates/terminal_view/src/terminal_element.rs +16 −3
  5. crates/ui_input/src/number_field.rs +45 −21
crates/editor/src/element.rs+86−20
@@ -46,9 +46,9 @@ use gpui::{ · 45 unmodified lines
46 KeybindingKeystroke, Length, Modifiers, ModifiersChangedEvent, MouseButton, MouseClickEvent,46 KeybindingKeystroke, Length, Modifiers, ModifiersChangedEvent, MouseButton, MouseClickEvent,
47 MouseDownEvent, MouseMoveEvent, MousePressureEvent, MouseUpEvent, PaintQuad, ParentElement,47 MouseDownEvent, MouseMoveEvent, MousePressureEvent, MouseUpEvent, PaintQuad, ParentElement,
48 Pixels, PressureStage, ScrollDelta, ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString,48 Pixels, PressureStage, ScrollDelta, ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString,
49 Size, StatefulInteractiveElement, Style, Styled, TextRun, TextStyleRefinement, WeakEntity,49 Size, StatefulInteractiveElement, Style, Styled, TextAlign, TextRun, TextStyleRefinement,
50 Window, anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px,50 WeakEntity, Window, anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline,
51 quad, relative, size, solid_background, transparent_black,51 point, px, quad, relative, size, solid_background, transparent_black,
52};52};
53use itertools::Itertools;53use itertools::Itertools;
54use language::{IndentGuideSettings, language_settings::ShowWhitespaceSetting};54use language::{IndentGuideSettings, language_settings::ShowWhitespaceSetting};
@@ -1695,9 +1695,13 @@ impl EditorElement { · 1640 unmodified lines
1695 [cursor_position.row().minus(visible_display_row_range.start) as usize];1695 [cursor_position.row().minus(visible_display_row_range.start) as usize];
1696 let cursor_column = cursor_position.column() as usize;1696 let cursor_column = cursor_position.column() as usize;
1697 1697
1698 let cursor_character_x = cursor_row_layout.x_for_index(cursor_column);1698 let cursor_character_x = cursor_row_layout.x_for_index(cursor_column)
1699 let mut block_width =1699 + cursor_row_layout
1700 cursor_row_layout.x_for_index(cursor_column + 1) - cursor_character_x;1700 .alignment_offset(self.style.text.text_align, text_hitbox.size.width);
1701 let cursor_next_x = cursor_row_layout.x_for_index(cursor_column + 1)
1702 + cursor_row_layout
1703 .alignment_offset(self.style.text.text_align, text_hitbox.size.width);
1704 let mut block_width = cursor_next_x - cursor_character_x;
1701 if block_width == Pixels::ZERO {1705 if block_width == Pixels::ZERO {
1702 block_width = em_advance;1706 block_width = em_advance;
1703 }1707 }
@@ -6160,10 +6164,25 @@ impl EditorElement { · 4456 unmodified lines
6160 let color = cx.theme().colors().editor_hover_line_number;6164 let color = cx.theme().colors().editor_hover_line_number;
6161 6165
6162 let line = self.shape_line_number(shaped_line.text.clone(), color, window);6166 let line = self.shape_line_number(shaped_line.text.clone(), color, window);
6163 line.paint(hitbox.origin, line_height, window, cx).log_err()6167 line.paint(
6168 hitbox.origin,
6169 line_height,
6170 TextAlign::Left,
6171 None,
6172 window,
6173 cx,
6174 )
6175 .log_err()
6164 } else {6176 } else {
6165 shaped_line6177 shaped_line
6166 .paint(hitbox.origin, line_height, window, cx)6178 .paint(
6179 hitbox.origin,
6180 line_height,
6181 TextAlign::Left,
6182 None,
6183 window,
6184 cx,
6185 )
6167 .log_err()6186 .log_err()
6168 }) else {6187 }) else {
6169 continue;6188 continue;
@@ -7252,23 +7271,27 @@ impl EditorElement { · 1082 unmodified lines
7252 .map(|row| {7271 .map(|row| {
7253 let line_layout =7272 let line_layout =
7254 &layout.position_map.line_layouts[row.minus(start_row) as usize];7273 &layout.position_map.line_layouts[row.minus(start_row) as usize];
7274 let alignment_offset =
7275 line_layout.alignment_offset(layout.text_align, layout.content_width);
7255 HighlightedRangeLine {7276 HighlightedRangeLine {
7256 start_x: if row == range.start.row() {7277 start_x: if row == range.start.row() {
7257 layout.content_origin.x7278 layout.content_origin.x
7258 + Pixels::from(7279 + Pixels::from(
7259 ScrollPixelOffset::from(7280 ScrollPixelOffset::from(
7260 line_layout.x_for_index(range.start.column() as usize),7281 line_layout.x_for_index(range.start.column() as usize)
7282 + alignment_offset,
7261 ) - layout.position_map.scroll_pixel_position.x,7283 ) - layout.position_map.scroll_pixel_position.x,
7262 )7284 )
7263 } else {7285 } else {
7264 layout.content_origin.x7286 layout.content_origin.x + alignment_offset
7265 - Pixels::from(layout.position_map.scroll_pixel_position.x)7287 - Pixels::from(layout.position_map.scroll_pixel_position.x)
7266 },7288 },
7267 end_x: if row == range.end.row() {7289 end_x: if row == range.end.row() {
7268 layout.content_origin.x7290 layout.content_origin.x
7269 + Pixels::from(7291 + Pixels::from(
7270 ScrollPixelOffset::from(7292 ScrollPixelOffset::from(
7271 line_layout.x_for_index(range.end.column() as usize),7293 line_layout.x_for_index(range.end.column() as usize)
7294 + alignment_offset,
7272 ) - layout.position_map.scroll_pixel_position.x,7295 ) - layout.position_map.scroll_pixel_position.x,
7273 )7296 )
7274 } else {7297 } else {
@@ -7276,6 +7299,7 @@ impl EditorElement { · 1 unmodified line
7276 ScrollPixelOffset::from(7299 ScrollPixelOffset::from(
7277 layout.content_origin.x7300 layout.content_origin.x
7278 + line_layout.width7301 + line_layout.width
7302 + alignment_offset
7279 + line_end_overshoot,7303 + line_end_overshoot,
7280 ) - layout.position_map.scroll_pixel_position.x,7304 ) - layout.position_map.scroll_pixel_position.x,
7281 )7305 )
@@ -8516,8 +8540,15 @@ impl LineWithInvisibles { · 1234 unmodified lines
8516 for fragment in &self.fragments {8540 for fragment in &self.fragments {
8517 match fragment {8541 match fragment {
8518 LineFragment::Text(line) => {8542 LineFragment::Text(line) => {
8519 line.paint(fragment_origin, line_height, window, cx)8543 line.paint(
8520 .log_err();8544 fragment_origin,
8545 line_height,
8546 layout.text_align,
8547 Some(layout.content_width),
8548 window,
8549 cx,
8550 )
8551 .log_err();
8521 fragment_origin.x += line.width;8552 fragment_origin.x += line.width;
8522 }8553 }
8523 LineFragment::Element { size, .. } => {8554 LineFragment::Element { size, .. } => {
@@ -8559,8 +8590,15 @@ impl LineWithInvisibles { · 35 unmodified lines
8559 for fragment in &self.fragments {8590 for fragment in &self.fragments {
8560 match fragment {8591 match fragment {
8561 LineFragment::Text(line) => {8592 LineFragment::Text(line) => {
8562 line.paint_background(fragment_origin, line_height, window, cx)8593 line.paint_background(
8563 .log_err();8594 fragment_origin,
8595 line_height,
8596 layout.text_align,
8597 Some(layout.content_width),
8598 window,
8599 cx,
8600 )
8601 .log_err();
8564 fragment_origin.x += line.width;8602 fragment_origin.x += line.width;
8565 }8603 }
8566 LineFragment::Element { size, .. } => {8604 LineFragment::Element { size, .. } => {
@@ -8609,7 +8647,7 @@ impl LineWithInvisibles { · 42 unmodified lines
8609 [token_offset, token_end_offset],8647 [token_offset, token_end_offset],
8610 Box::new(move |window: &mut Window, cx: &mut App| {8648 Box::new(move |window: &mut Window, cx: &mut App| {
8611 invisible_symbol8649 invisible_symbol
8612 .paint(origin, line_height, window, cx)8650 .paint(origin, line_height, TextAlign::Left, None, window, cx)
8613 .log_err();8651 .log_err();
8614 }),8652 }),
8615 )8653 )
@@ -8770,6 +8808,15 @@ impl LineWithInvisibles { · 154 unmodified lines
8770 8808
8771 None8809 None
8772 }8810 }
8811
8812 pub fn alignment_offset(&self, text_align: TextAlign, content_width: Pixels) -> Pixels {
8813 let line_width = self.width;
8814 match text_align {
8815 TextAlign::Left => px(0.0),
8816 TextAlign::Center => (content_width - line_width) / 2.0,
8817 TextAlign::Right => content_width - line_width,
8818 }
8819 }
8773}8820}
8774 8821
8775#[derive(Debug, Clone, Copy, PartialEq, Eq)]8822#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -10172,6 +10219,8 @@ impl Element for EditorElement { · 1396 unmodified lines
10172 em_width,10219 em_width,
10173 em_advance,10220 em_advance,
10174 snapshot,10221 snapshot,
10222 text_align: self.style.text.text_align,
10223 content_width: text_hitbox.size.width,
10175 gutter_hitbox: gutter_hitbox.clone(),10224 gutter_hitbox: gutter_hitbox.clone(),
10176 text_hitbox: text_hitbox.clone(),10225 text_hitbox: text_hitbox.clone(),
10177 inline_blame_bounds: inline_blame_layout10226 inline_blame_bounds: inline_blame_layout
@@ -10225,6 +10274,8 @@ impl Element for EditorElement { · 47 unmodified lines
10225 sticky_buffer_header,10274 sticky_buffer_header,
10226 sticky_headers,10275 sticky_headers,
10227 expand_toggles,10276 expand_toggles,
10277 text_align: self.style.text.text_align,
10278 content_width: text_hitbox.size.width,
10228 }10279 }
10229 })10280 })
10230 })10281 })
@@ -10405,6 +10456,8 @@ pub struct EditorLayout { · 174 unmodified lines
10405 sticky_buffer_header: Option<AnyElement>,10456 sticky_buffer_header: Option<AnyElement>,
10406 sticky_headers: Option<StickyHeaders>,10457 sticky_headers: Option<StickyHeaders>,
10407 document_colors: Option<(DocumentColorsRenderMode, Vec<(Range<DisplayPoint>, Hsla)>)>,10458 document_colors: Option<(DocumentColorsRenderMode, Vec<(Range<DisplayPoint>, Hsla)>)>,
10459 text_align: TextAlign,
10460 content_width: Pixels,
10408}10461}
10409 10462
10410struct StickyHeaders {10463struct StickyHeaders {
@@ -10572,7 +10625,9 @@ impl StickyHeaderLine { · 161 unmodified lines
10572 gutter_origin.x + gutter_width - gutter_right_padding - line_number.width,10625 gutter_origin.x + gutter_width - gutter_right_padding - line_number.width,
10573 gutter_origin.y,10626 gutter_origin.y,
10574 );10627 );
10575 line_number.paint(origin, line_height, window, cx).log_err();10628 line_number
10629 .paint(origin, line_height, TextAlign::Left, None, window, cx)
10630 .log_err();
10576 }10631 }
10577 }10632 }
10578}10633}
@@ -11011,6 +11066,8 @@ pub(crate) struct PositionMap { · 432 unmodified lines
11011 pub visible_row_range: Range<DisplayRow>,11066 pub visible_row_range: Range<DisplayRow>,
11012 pub line_layouts: Vec<LineWithInvisibles>,11067 pub line_layouts: Vec<LineWithInvisibles>,
11013 pub snapshot: EditorSnapshot,11068 pub snapshot: EditorSnapshot,
11069 pub text_align: TextAlign,
11070 pub content_width: Pixels,
11014 pub text_hitbox: Hitbox,11071 pub text_hitbox: Hitbox,
11015 pub gutter_hitbox: Hitbox,11072 pub gutter_hitbox: Hitbox,
11016 pub inline_blame_bounds: Option<(Bounds<Pixels>, BufferId, BlameEntry)>,11073 pub inline_blame_bounds: Option<(Bounds<Pixels>, BufferId, BlameEntry)>,
@@ -11076,10 +11133,12 @@ impl PositionMap { · 59 unmodified lines
11076 .line_layouts11133 .line_layouts
11077 .get(row as usize - scroll_position.y as usize)11134 .get(row as usize - scroll_position.y as usize)
11078 {11135 {
11079 if let Some(ix) = line.index_for_x(x) {11136 let alignment_offset = line.alignment_offset(self.text_align, self.content_width);
11137 let x_relative_to_text = x - alignment_offset;
11138 if let Some(ix) = line.index_for_x(x_relative_to_text) {
11080 (ix as u32, px(0.))11139 (ix as u32, px(0.))
11081 } else {11140 } else {
11082 (line.len as u32, px(0.).max(x - line.width))11141 (line.len as u32, px(0.).max(x_relative_to_text - line.width))
11083 }11142 }
11084 } else {11143 } else {
11085 (0, x)11144 (0, x)
@@ -11268,7 +11327,14 @@ impl CursorLayout { · 182 unmodified lines
11268 11327
11269 if let Some(block_text) = &self.block_text {11328 if let Some(block_text) = &self.block_text {
11270 block_text11329 block_text
11271 .paint(self.origin + origin, self.line_height, window, cx)11330 .paint(
11331 self.origin + origin,
11332 self.line_height,
11333 TextAlign::Left,
11334 None,
11335 window,
11336 cx,
11337 )
11272 .log_err();11338 .log_err();
11273 }11339 }
11274 }11340 }
crates/gpui/examples/input.rs+9−2
@@ -546,8 +546,15 @@ impl Element for TextElement { · 545 unmodified lines
546 window.paint_quad(selection)546 window.paint_quad(selection)
547 }547 }
548 let line = prepaint.line.take().unwrap();548 let line = prepaint.line.take().unwrap();
549 line.paint(bounds.origin, window.line_height(), window, cx)549 line.paint(
550 .unwrap();550 bounds.origin,
551 window.line_height(),
552 gpui::TextAlign::Left,
553 None,
554 window,
555 cx,
556 )
557 .unwrap();
551 558
552 if focus_handle.is_focused(window)559 if focus_handle.is_focused(window)
553 && let Some(cursor) = prepaint.cursor.take()560 && let Some(cursor) = prepaint.cursor.take()
crates/gpui/src/text_system/line.rs+8−4
@@ -64,6 +64,8 @@ impl ShapedLine { · 63 unmodified lines
64 &self,64 &self,
65 origin: Point<Pixels>,65 origin: Point<Pixels>,
66 line_height: Pixels,66 line_height: Pixels,
67 align: TextAlign,
68 align_width: Option<Pixels>,
67 window: &mut Window,69 window: &mut Window,
68 cx: &mut App,70 cx: &mut App,
69 ) -> Result<()> {71 ) -> Result<()> {
@@ -71,8 +73,8 @@ impl ShapedLine { · 1 unmodified line
71 origin,73 origin,
72 &self.layout,74 &self.layout,
73 line_height,75 line_height,
74 TextAlign::default(),76 align,
75 None,77 align_width,
76 &self.decoration_runs,78 &self.decoration_runs,
77 &[],79 &[],
78 window,80 window,
@@ -87,6 +89,8 @@ impl ShapedLine { · 8 unmodified lines
87 &self,89 &self,
88 origin: Point<Pixels>,90 origin: Point<Pixels>,
89 line_height: Pixels,91 line_height: Pixels,
92 align: TextAlign,
93 align_width: Option<Pixels>,
90 window: &mut Window,94 window: &mut Window,
91 cx: &mut App,95 cx: &mut App,
92 ) -> Result<()> {96 ) -> Result<()> {
@@ -94,8 +98,8 @@ impl ShapedLine { · 1 unmodified line
94 origin,98 origin,
95 &self.layout,99 &self.layout,
96 line_height,100 line_height,
97 TextAlign::default(),101 align,
98 None,102 align_width,
99 &self.decoration_runs,103 &self.decoration_runs,
100 &[],104 &[],
101 window,105 window,
crates/terminal_view/src/terminal_element.rs+16−3
@@ -151,7 +151,14 @@ impl BatchedTextRun { · 150 unmodified lines
151 std::slice::from_ref(&self.style),151 std::slice::from_ref(&self.style),
152 Some(dimensions.cell_width),152 Some(dimensions.cell_width),
153 )153 )
154 .paint(pos, dimensions.line_height, window, cx);154 .paint(
155 pos,
156 dimensions.line_height,
157 gpui::TextAlign::Left,
158 None,
159 window,
160 cx,
161 );
155 }162 }
156}163}
157 164
@@ -1326,8 +1333,14 @@ impl Element for TerminalElement { · 1168 unmodified lines
1326 }],1333 }],
1327 None1334 None
1328 );1335 );
1329 shaped_line1336 shaped_line.paint(
1330 .paint(ime_position, layout.dimensions.line_height, window, cx)1337 ime_position,
1338 layout.dimensions.line_height,
1339 gpui::TextAlign::Left,
1340 None,
1341 window,
1342 cx,
1343 )
1331 .log_err();1344 .log_err();
1332 }1345 }
1333 1346
crates/ui_input/src/number_field.rs+45−21
@@ -5,8 +5,11 @@ use std::{ · 4 unmodified lines
5 str::FromStr,5 str::FromStr,
6};6};
7 7
8use editor::{Editor, EditorStyle};8use editor::Editor;
9use gpui::{ClickEvent, Entity, FocusHandle, Focusable, FontWeight, Modifiers};9use gpui::{
10 ClickEvent, Entity, FocusHandle, Focusable, FontWeight, Modifiers, TextAlign,
11 TextStyleRefinement,
12};
10 13
11use settings::{CenteredPaddingSettings, CodeFade, DelayMs, InactiveOpacity, MinimumContrast};14use settings::{CenteredPaddingSettings, CodeFade, DelayMs, InactiveOpacity, MinimumContrast};
12use ui::prelude::*;15use ui::prelude::*;
@@ -309,6 +312,11 @@ impl<T: NumberFieldType> NumberField<T> { · 296 unmodified lines
309 self312 self
310 }313 }
311 314
315 pub fn mode(self, mode: NumberFieldMode, cx: &mut App) -> Self {
316 self.mode.write(cx, mode);
317 self
318 }
319
312 pub fn on_reset(320 pub fn on_reset(
313 mut self,321 mut self,
314 on_reset: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,322 on_reset: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
@@ -451,9 +459,11 @@ impl<T: NumberFieldType> RenderOnce for NumberField<T> { · 136 unmodified lines
451 |window, cx| {459 |window, cx| {
452 let previous_focus_handle = window.focused(cx);460 let previous_focus_handle = window.focused(cx);
453 let mut editor = Editor::single_line(window, cx);461 let mut editor = Editor::single_line(window, cx);
454 let mut style = EditorStyle::default();462
455 style.text.text_align = gpui::TextAlign::Right;463 editor.set_text_style_refinement(TextStyleRefinement {
456 editor.set_style(style, window, cx);464 text_align: Some(TextAlign::Center),
465 ..Default::default()
466 });
457 467
458 editor.set_text(format!("{}", self.value), window, cx);468 editor.set_text(format!("{}", self.value), window, cx);
459 cx.on_focus_out(&editor.focus_handle(cx), window, {469 cx.on_focus_out(&editor.focus_handle(cx), window, {
@@ -555,22 +565,36 @@ impl Component for NumberField<usize> { · 95 unmodified lines
555 Some(565 Some(
556 v_flex()566 v_flex()
557 .gap_6()567 .gap_6()
558 .children(vec![single_example(568 .children(vec![
559 "Default Numeric Stepper",569 single_example(
560 NumberField::new(570 "Default Number Field",
561 "numeric-stepper-component-preview",571 NumberField::new("number-field", *stepper_example.read(cx), window, cx)
562 *stepper_example.read(cx),572 .on_change({
563 window,573 let stepper_example = stepper_example.clone();
564 cx,574 move |value, _, cx| stepper_example.write(cx, *value)
565 )575 })
566 .on_change({576 .min(1.0)
567 let stepper_example = stepper_example.clone();577 .max(100.0)
568 move |value, _, cx| stepper_example.write(cx, *value)578 .into_any_element(),
569 })579 ),
570 .min(1.0)580 single_example(
571 .max(100.0)581 "Read-Only Number Field",
572 .into_any_element(),582 NumberField::new(
573 )])583 "editable-number-field",
584 *stepper_example.read(cx),
585 window,
586 cx,
587 )
588 .on_change({
589 let stepper_example = stepper_example.clone();
590 move |value, _, cx| stepper_example.write(cx, *value)
591 })
592 .min(1.0)
593 .max(100.0)
594 .mode(NumberFieldMode::Edit, cx)
595 .into_any_element(),
596 ),
597 ])
574 .into_any_element(),598 .into_any_element(),
575 )599 )
576 }600 }