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:
parent
4a65315f3b
commit
aa42e206b3
5 changed files with 112 additions and 6 deletions
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue