@@ -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::<crate::DebugBelow>())
&& 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,
@@ -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<TextOverflow>,
+
+ /// 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<usize>,
}
@@ -362,6 +380,7 @@ impl Default for TextStyle {
strikethrough: None,
white_space: WhiteSpace::Normal,
text_overflow: None,
+ text_align: TextAlign::default(),
line_clamp: None,
}
}
@@ -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 {
@@ -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<Pixels>,
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<Pixels>,
layout: &LineLayout,
line_height: Pixels,
+ align: TextAlign,
+ align_width: Option<Pixels>,
decoration_runs: &[DecorationRun],
wrap_boundaries: &[WrapBoundary],
window: &mut Window,
@@ -147,7 +156,17 @@ fn paint_line(
let mut current_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None;
let mut current_background: Option<(Point<Pixels>, 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<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,
+ }
+}