From aa42e206b3f06a2b70567118d9d70dc0e279fece Mon Sep 17 00:00:00 2001 From: someone13574 <81528246+someone13574@users.noreply.github.com> Date: Sun, 2 Feb 2025 12:15:12 -0500 Subject: [PATCH] gpui: Add text alignment (#24090) Adds a text property for controlling left, center, or right text alignment. #8792 should stay open since this doesn't add support for `justify` (which would require a much bigger change since this can just alter the origin of each line, but justify requires changing spacing, whereas justify requires changes to each platform's shaping code). Release Notes: - N/A --- crates/gpui/src/elements/div.rs | 5 ++- crates/gpui/src/elements/text.rs | 4 +- crates/gpui/src/style.rs | 19 +++++++++ crates/gpui/src/styled.rs | 25 ++++++++++- crates/gpui/src/text_system/line.rs | 65 +++++++++++++++++++++++++++-- 5 files changed, 112 insertions(+), 6 deletions(-) diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index 4f12a6837a..f9ff17ea3c 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -1661,6 +1661,8 @@ impl Interactivity { window: &mut Window, cx: &mut App, ) { + use crate::TextAlign; + if global_id.is_some() && (style.debug || style.debug_below || cx.has_global::()) && hitbox.is_hovered(window) @@ -1682,7 +1684,8 @@ impl Interactivity { .ok() .and_then(|mut text| text.pop()) { - text.paint(hitbox.origin, FONT_SIZE, window, cx).ok(); + text.paint(hitbox.origin, FONT_SIZE, TextAlign::Left, window, cx) + .ok(); let text_bounds = crate::Bounds { origin: hitbox.origin, diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index b43a82dff6..36771ed5ec 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -390,8 +390,10 @@ impl TextLayout { let line_height = element_state.line_height; let mut line_origin = bounds.origin; + let text_style = window.text_style(); for line in &element_state.lines { - line.paint(line_origin, line_height, window, cx).log_err(); + line.paint(line_origin, line_height, text_style.text_align, window, cx) + .log_err(); line_origin.y += line.size(line_height).height; } } diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index 2f9fa67799..8c1c20c2ac 100644 --- a/crates/gpui/src/style.rs +++ b/crates/gpui/src/style.rs @@ -293,6 +293,20 @@ pub enum TextOverflow { Ellipsis(&'static str), } +/// How to align text within the element +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub enum TextAlign { + /// Align the text to the left of the element + #[default] + Left, + + /// Center the text within the element + Center, + + /// Align the text to the right of the element + Right, +} + /// The properties that can be used to style text in GPUI #[derive(Refineable, Clone, Debug, PartialEq)] #[refineable(Debug)] @@ -335,6 +349,10 @@ pub struct TextStyle { /// The text should be truncated if it overflows the width of the element pub text_overflow: Option, + + /// How the text should be aligned within the element + pub text_align: TextAlign, + /// The number of lines to display before truncating the text pub line_clamp: Option, } @@ -362,6 +380,7 @@ impl Default for TextStyle { strikethrough: None, white_space: WhiteSpace::Normal, text_overflow: None, + text_align: TextAlign::default(), line_clamp: None, } } diff --git a/crates/gpui/src/styled.rs b/crates/gpui/src/styled.rs index b3af2c5055..c1f6a9f617 100644 --- a/crates/gpui/src/styled.rs +++ b/crates/gpui/src/styled.rs @@ -1,9 +1,9 @@ -use crate::TextStyleRefinement; use crate::{ self as gpui, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle, DefiniteLength, Fill, FlexDirection, FlexWrap, Font, FontStyle, FontWeight, Hsla, JustifyContent, Length, SharedString, StrikethroughStyle, StyleRefinement, TextOverflow, WhiteSpace, }; +use crate::{TextAlign, TextStyleRefinement}; pub use gpui_macros::{ border_style_methods, box_shadow_style_methods, cursor_style_methods, margin_style_methods, overflow_style_methods, padding_style_methods, position_style_methods, @@ -78,6 +78,29 @@ pub trait Styled: Sized { self } + /// Set the text alignment of the element. + fn text_align(mut self, align: TextAlign) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .text_align = Some(align); + self + } + + /// Sets the text alignment to left + fn text_left(mut self) -> Self { + self.text_align(TextAlign::Left) + } + + /// Sets the text alignment to center + fn text_center(mut self) -> Self { + self.text_align(TextAlign::Center) + } + + /// Sets the text alignment to right + fn text_right(mut self) -> Self { + self.text_align(TextAlign::Right) + } + /// Sets the truncate to prevent text from wrapping and truncate overflowing text with an ellipsis (…) if needed. /// [Docs](https://tailwindcss.com/docs/text-overflow#truncate) fn truncate(mut self) -> Self { diff --git a/crates/gpui/src/text_system/line.rs b/crates/gpui/src/text_system/line.rs index 94708f1574..5c14565c15 100644 --- a/crates/gpui/src/text_system/line.rs +++ b/crates/gpui/src/text_system/line.rs @@ -1,6 +1,7 @@ use crate::{ black, fill, point, px, size, App, Bounds, Half, Hsla, LineLayout, Pixels, Point, Result, - SharedString, StrikethroughStyle, UnderlineStyle, Window, WrapBoundary, WrappedLineLayout, + SharedString, StrikethroughStyle, TextAlign, UnderlineStyle, Window, WrapBoundary, + WrappedLineLayout, }; use derive_more::{Deref, DerefMut}; use smallvec::SmallVec; @@ -70,6 +71,8 @@ impl ShapedLine { origin, &self.layout, line_height, + TextAlign::default(), + None, &self.decoration_runs, &[], window, @@ -103,6 +106,7 @@ impl WrappedLine { &self, origin: Point, line_height: Pixels, + align: TextAlign, window: &mut Window, cx: &mut App, ) -> Result<()> { @@ -110,6 +114,8 @@ impl WrappedLine { origin, &self.layout.unwrapped_layout, line_height, + align, + self.layout.wrap_width, &self.decoration_runs, &self.wrap_boundaries, window, @@ -120,10 +126,13 @@ impl WrappedLine { } } +#[allow(clippy::too_many_arguments)] fn paint_line( origin: Point, layout: &LineLayout, line_height: Pixels, + align: TextAlign, + align_width: Option, decoration_runs: &[DecorationRun], wrap_boundaries: &[WrapBoundary], window: &mut Window, @@ -147,7 +156,17 @@ fn paint_line( let mut current_strikethrough: Option<(Point, StrikethroughStyle)> = None; let mut current_background: Option<(Point, Hsla)> = None; let text_system = cx.text_system().clone(); - let mut glyph_origin = origin; + let mut glyph_origin = point( + aligned_origin_x( + origin, + align_width.unwrap_or(layout.width), + px(0.0), + &align, + layout, + wraps.peek(), + ), + origin.y, + ); let mut prev_glyph_position = Point::default(); let mut max_glyph_size = size(px(0.), px(0.)); for (run_ix, run) in layout.runs.iter().enumerate() { @@ -200,7 +219,14 @@ fn paint_line( strikethrough_origin.y += line_height; } - glyph_origin.x = origin.x; + glyph_origin.x = aligned_origin_x( + origin, + align_width.unwrap_or(layout.width), + prev_glyph_position.x, + &align, + layout, + wraps.peek(), + ); glyph_origin.y += line_height; } prev_glyph_position = glyph.position; @@ -390,3 +416,36 @@ fn paint_line( Ok(()) }) } + +fn aligned_origin_x( + origin: Point, + align_width: Pixels, + last_glyph_x: Pixels, + align: &TextAlign, + layout: &LineLayout, + wrap_boundary: Option<&&WrapBoundary>, +) -> Pixels { + let end_of_line = if let Some(WrapBoundary { run_ix, glyph_ix }) = wrap_boundary { + if layout.runs[*run_ix].glyphs.len() == glyph_ix + 1 { + // Next glyph is in next run + layout + .runs + .get(run_ix + 1) + .and_then(|run| run.glyphs.first()) + .map_or(layout.width, |glyph| glyph.position.x) + } else { + // Get next glyph + layout.runs[*run_ix].glyphs[*glyph_ix + 1].position.x + } + } else { + layout.width + }; + + let line_width = end_of_line - last_glyph_x; + + match align { + TextAlign::Left => origin.x, + TextAlign::Center => (2.0 * origin.x + align_width - line_width) / 2.0, + TextAlign::Right => origin.x + align_width - line_width, + } +}