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
This commit is contained in:
someone13574 2025-02-02 12:15:12 -05:00 committed by GitHub
parent 4a65315f3b
commit aa42e206b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 112 additions and 6 deletions

View file

@ -1661,6 +1661,8 @@ impl Interactivity {
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) { ) {
use crate::TextAlign;
if global_id.is_some() if global_id.is_some()
&& (style.debug || style.debug_below || cx.has_global::<crate::DebugBelow>()) && (style.debug || style.debug_below || cx.has_global::<crate::DebugBelow>())
&& hitbox.is_hovered(window) && hitbox.is_hovered(window)
@ -1682,7 +1684,8 @@ impl Interactivity {
.ok() .ok()
.and_then(|mut text| text.pop()) .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 { let text_bounds = crate::Bounds {
origin: hitbox.origin, origin: hitbox.origin,

View file

@ -390,8 +390,10 @@ impl TextLayout {
let line_height = element_state.line_height; let line_height = element_state.line_height;
let mut line_origin = bounds.origin; let mut line_origin = bounds.origin;
let text_style = window.text_style();
for line in &element_state.lines { 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; line_origin.y += line.size(line_height).height;
} }
} }

View file

@ -293,6 +293,20 @@ pub enum TextOverflow {
Ellipsis(&'static str), 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 /// The properties that can be used to style text in GPUI
#[derive(Refineable, Clone, Debug, PartialEq)] #[derive(Refineable, Clone, Debug, PartialEq)]
#[refineable(Debug)] #[refineable(Debug)]
@ -335,6 +349,10 @@ pub struct TextStyle {
/// The text should be truncated if it overflows the width of the element /// The text should be truncated if it overflows the width of the element
pub text_overflow: Option<TextOverflow>, pub text_overflow: Option<TextOverflow>,
/// How the text should be aligned within the element
pub text_align: TextAlign,
/// The number of lines to display before truncating the text /// The number of lines to display before truncating the text
pub line_clamp: Option<usize>, pub line_clamp: Option<usize>,
} }
@ -362,6 +380,7 @@ impl Default for TextStyle {
strikethrough: None, strikethrough: None,
white_space: WhiteSpace::Normal, white_space: WhiteSpace::Normal,
text_overflow: None, text_overflow: None,
text_align: TextAlign::default(),
line_clamp: None, line_clamp: None,
} }
} }

View file

@ -1,9 +1,9 @@
use crate::TextStyleRefinement;
use crate::{ use crate::{
self as gpui, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle, DefiniteLength, self as gpui, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle, DefiniteLength,
Fill, FlexDirection, FlexWrap, Font, FontStyle, FontWeight, Hsla, JustifyContent, Length, Fill, FlexDirection, FlexWrap, Font, FontStyle, FontWeight, Hsla, JustifyContent, Length,
SharedString, StrikethroughStyle, StyleRefinement, TextOverflow, WhiteSpace, SharedString, StrikethroughStyle, StyleRefinement, TextOverflow, WhiteSpace,
}; };
use crate::{TextAlign, TextStyleRefinement};
pub use gpui_macros::{ pub use gpui_macros::{
border_style_methods, box_shadow_style_methods, cursor_style_methods, margin_style_methods, border_style_methods, box_shadow_style_methods, cursor_style_methods, margin_style_methods,
overflow_style_methods, padding_style_methods, position_style_methods, overflow_style_methods, padding_style_methods, position_style_methods,
@ -78,6 +78,29 @@ pub trait Styled: Sized {
self 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. /// 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) /// [Docs](https://tailwindcss.com/docs/text-overflow#truncate)
fn truncate(mut self) -> Self { fn truncate(mut self) -> Self {

View file

@ -1,6 +1,7 @@
use crate::{ use crate::{
black, fill, point, px, size, App, Bounds, Half, Hsla, LineLayout, Pixels, Point, Result, 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 derive_more::{Deref, DerefMut};
use smallvec::SmallVec; use smallvec::SmallVec;
@ -70,6 +71,8 @@ impl ShapedLine {
origin, origin,
&self.layout, &self.layout,
line_height, line_height,
TextAlign::default(),
None,
&self.decoration_runs, &self.decoration_runs,
&[], &[],
window, window,
@ -103,6 +106,7 @@ impl WrappedLine {
&self, &self,
origin: Point<Pixels>, origin: Point<Pixels>,
line_height: Pixels, line_height: Pixels,
align: TextAlign,
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> Result<()> { ) -> Result<()> {
@ -110,6 +114,8 @@ impl WrappedLine {
origin, origin,
&self.layout.unwrapped_layout, &self.layout.unwrapped_layout,
line_height, line_height,
align,
self.layout.wrap_width,
&self.decoration_runs, &self.decoration_runs,
&self.wrap_boundaries, &self.wrap_boundaries,
window, window,
@ -120,10 +126,13 @@ impl WrappedLine {
} }
} }
#[allow(clippy::too_many_arguments)]
fn paint_line( fn paint_line(
origin: Point<Pixels>, origin: Point<Pixels>,
layout: &LineLayout, layout: &LineLayout,
line_height: Pixels, line_height: Pixels,
align: TextAlign,
align_width: Option<Pixels>,
decoration_runs: &[DecorationRun], decoration_runs: &[DecorationRun],
wrap_boundaries: &[WrapBoundary], wrap_boundaries: &[WrapBoundary],
window: &mut Window, window: &mut Window,
@ -147,7 +156,17 @@ fn paint_line(
let mut current_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None; let mut current_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None;
let mut current_background: Option<(Point<Pixels>, Hsla)> = None; let mut current_background: Option<(Point<Pixels>, Hsla)> = None;
let text_system = cx.text_system().clone(); 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 prev_glyph_position = Point::default();
let mut max_glyph_size = size(px(0.), px(0.)); let mut max_glyph_size = size(px(0.), px(0.));
for (run_ix, run) in layout.runs.iter().enumerate() { for (run_ix, run) in layout.runs.iter().enumerate() {
@ -200,7 +219,14 @@ fn paint_line(
strikethrough_origin.y += line_height; 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; glyph_origin.y += line_height;
} }
prev_glyph_position = glyph.position; prev_glyph_position = glyph.position;
@ -390,3 +416,36 @@ fn paint_line(
Ok(()) Ok(())
}) })
} }
fn aligned_origin_x(
origin: Point<Pixels>,
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,
}
}