Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions crates/gpui/examples/inline_text.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use gpui::{
div, prelude::*, px, size, App, Application, Bounds, Context, InlineText, Window, WindowBounds,
WindowOptions,
};

struct RootView;

impl Render for RootView {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.id("page")
.size_full()
.flex()
.flex_col()
.p_2()
.gap_2()
.bg(gpui::black())
.text_color(gpui::white())
.child(
div().border_1().border_color(gpui::red()).child(
InlineText::new()

@huacnlee huacnlee Mar 10, 2025 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use span, Span::new, like the before div.

And the next add_text, add_element.

I think we can try to have a better API like this if it's possible:

div().id("paragraph")
  .child(span("This is an inline element ->"))
  .child(span().w_4().bg(gpui::red()))
  .child(span("text"))

Or at least we can:

div().id("paragraph")
  .child(
    InlineText::new()
      .child("This is an inline element ->")
      .child(div().w_4().bg(gpui::red()))
      .child("text")
  )

.add_text("This is an inline element ->")
.add_element(|_window| {
div()
.h_4()
.w_4()
.rounded_full()
.bg(gpui::red())
.into_any_element()
})
.add_text("<-")
.add_text(" This is an inline element ->")
.add_element(|_window| {
div()
.h_4()
.w_12()
.bg(gpui::green())
.flex()
.justify_center()
.text_xs()
.child("😊")
.into_any_element()
})
.add_text("<-")
.add_text(" This is an inline element ->")
.add_element(|_window| {
div()
.h_4()
.w_5()
.bg(gpui::blue())
.flex()
.justify_center()
.text_xs()
.child("😊")
.into_any_element()
})
.add_text("<-")
.add_text(" This is an inline element ->")
.add_element(|_window| {
div()
.h_4()
.w_24()
.bg(gpui::yellow())
.flex()
.justify_center()
.text_xs()
.child("😊")
.into_any_element()
})
.add_text("<-"),
),
)
.child(
div().border_1().border_color(gpui::red()).child(
InlineText::new()
.add_text(" This is a tall inline element ->")
.add_element(|_window| {
div()
.h_7()
.w_16()
.bg(gpui::white())
.text_color(gpui::black())
.text_center()
.text_2xl()
.child("TALL")
.into_any_element()
})
.add_text("<-"),
),
)
}
}

fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(800.0), px(600.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|_| RootView),
)
.unwrap();
cx.activate(true);
});
}
1 change: 1 addition & 0 deletions crates/gpui/src/elements/div.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1681,6 +1681,7 @@ impl Interactivity {
&[window.text_style().to_run(str_len)],
None,
None,
None,
)
.ok()
.and_then(|mut text| text.pop())
Expand Down
210 changes: 200 additions & 10 deletions crates/gpui/src/elements/text.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use crate::{
register_tooltip_mouse_handlers, set_tooltip_on_window, ActiveTooltip, AnyView, App, Bounds,
DispatchPhase, Element, ElementId, GlobalElementId, HighlightStyle, Hitbox, IntoElement,
LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, SharedString, Size,
TextOverflow, TextRun, TextStyle, TooltipId, WhiteSpace, Window, WrappedLine,
WrappedLineLayout,
point, px, register_tooltip_mouse_handlers, set_tooltip_on_window, size, ActiveTooltip,
AnyElement, AnyView, App, AvailableSpace, Bounds, DispatchPhase, Element, ElementId,
GlobalElementId, HighlightStyle, Hitbox, InlineBox, IntoElement, LayoutId, MouseDownEvent,
MouseMoveEvent, MouseUpEvent, Pixels, Point, SharedString, Size, Style, TextOverflow, TextRun,
TextStyle, TooltipId, WhiteSpace, Window, WrappedLine, WrappedLineLayout,
};
use anyhow::anyhow;
use parking_lot::{Mutex, MutexGuard};
Expand Down Expand Up @@ -32,7 +32,7 @@ impl Element for &'static str {
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let mut state = TextLayout::default();
let layout_id = state.layout(SharedString::from(*self), None, window, cx);
let layout_id = state.layout(SharedString::from(*self), None, None, window, cx);
(layout_id, state)
}

Expand Down Expand Up @@ -93,7 +93,7 @@ impl Element for SharedString {
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let mut state = TextLayout::default();
let layout_id = state.layout(self.clone(), None, window, cx);
let layout_id = state.layout(self.clone(), None, None, window, cx);
(layout_id, state)
}

Expand Down Expand Up @@ -239,7 +239,9 @@ impl Element for StyledText {
})
});

let layout_id = self.layout.layout(self.text.clone(), runs, window, cx);
let layout_id = self
.layout
.layout(self.text.clone(), runs, None, window, cx);
(layout_id, ())
}

Expand Down Expand Up @@ -296,6 +298,7 @@ impl TextLayout {
&self,
text: SharedString,
runs: Option<Vec<TextRun>>,
inline_boxes: Option<Vec<InlineBox>>,
window: &mut Window,
_: &mut App,
) -> LayoutId {
Expand Down Expand Up @@ -349,9 +352,19 @@ impl TextLayout {
}
}

let inline_boxes_width = inline_boxes
.as_deref()
.unwrap_or_default()
.iter()
.fold(px(0.), |width, b| width + b.size.width);
let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size);
let text = if let Some(truncate_width) = truncate_width {
line_wrapper.truncate_line(text.clone(), truncate_width, ellipsis, &mut runs)
line_wrapper.truncate_line(
text.clone(),
truncate_width - inline_boxes_width,
ellipsis,
&mut runs,
)
} else {
text.clone()
};
Expand All @@ -362,6 +375,7 @@ impl TextLayout {
text,
font_size,
&runs,
inline_boxes.as_deref(),
wrap_width, // Wrap if we know the width.
text_style.line_clamp, // Limit the number of lines if line_clamp is set.
)
Expand All @@ -381,7 +395,7 @@ impl TextLayout {
for line in &lines {
let line_size = line.size(line_height);
size.height += line_size.height;
size.width = size.width.max(line_size.width).ceil();
size.width = size.width.max(line_size.width + inline_boxes_width).ceil();
}

element_state.lock().replace(TextLayoutInner {
Expand Down Expand Up @@ -436,6 +450,45 @@ impl TextLayout {
}
}

/// Get the position of a specified inline box index.
pub fn position_of_inline_box(&self, index: usize) -> Option<Point<Pixels>> {
let element_state = self.lock();
let element_state = element_state
.as_ref()
.expect("measurement has not been performed");
let bounds = element_state
.bounds
.expect("prepaint has not been performed");
let line_height = element_state.line_height;

for line in &element_state.lines {
if let Some(inline_box) = line.inline_boxes.get(index) {
let origin = line.position_for_index(inline_box.glyph_ix, line_height);
let should_remove_width = line
.runs()
.get(inline_box.run_ix)
.map(|run| run.glyphs.len() != inline_box.glyph_ix)
.unwrap_or(false);

if let Some(origin) = origin {
return Some(point(
bounds.origin.x + origin.x
- if should_remove_width {
inline_box.size.width
} else {
px(0.)
},
bounds.origin.y + origin.y + line_height
- inline_box.size.height
- line.descent(),
));
}
}
}

None
}

/// Get the byte index into the input of the pixel position.
pub fn index_for_position(&self, mut position: Point<Pixels>) -> Result<usize, usize> {
let element_state = self.lock();
Expand Down Expand Up @@ -836,3 +889,140 @@ impl IntoElement for InteractiveText {
self
}
}

/// A text element that can have inline elements.
pub struct InlineText {
layout: TextLayout,
text: SharedString,
inline_items: Vec<(Box<dyn FnMut(&mut Window) -> AnyElement>, InlineBox)>,
}

impl InlineText {
/// Create a new `InlineText``.
pub fn new() -> Self {
InlineText {
layout: TextLayout::default(),
text: SharedString::new(""),
inline_items: Vec::new(),
}
}

/// Add a text run to the `InlineText``.
pub fn add_text(mut self, text: impl Into<SharedString>) -> Self {
self.text = SharedString::from(format!("{}{}", self.text, text.into()));
self
}

/// Add a element run to the `InlineText``.
pub fn add_element<R>(mut self, element: R) -> Self
where
R: 'static + Fn(&mut Window) -> AnyElement,
{
self.inline_items.push((
Box::new(element),
InlineBox {
run_ix: 0,
glyph_ix: self.text.chars().count(),
size: Size::default(),
},
));
self
}
}

/// The layout state that is created when layouting `InlineText`.
pub struct InlineTextLayoutState {
inline_elements: Vec<AnyElement>,
}

impl Element for InlineText {
type RequestLayoutState = InlineTextLayoutState;
type PrepaintState = ();

fn id(&self) -> Option<ElementId> {
None
}

fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
window: &mut Window,
app: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let mut inline_elements = Vec::new();
let mut inline_boxes = Vec::new();

for inline_element in &mut self.inline_items {
let mut element = inline_element.0(window);
let layout_id = element.request_layout(window, app);

window.compute_layout(
layout_id,
size(AvailableSpace::MaxContent, AvailableSpace::MaxContent),
app,
);
let inline_element_bounds = window.layout_bounds(layout_id);

inline_elements.push(element);
let mut inline_box = inline_element.1.clone();
inline_box.size = inline_element_bounds.size;

inline_boxes.push(inline_box);
}

let text_layout_id =
self.layout
.layout(self.text.clone(), None, Some(inline_boxes), window, app);

let style = Style::default();
(
window.request_layout(style, [text_layout_id], app),
Self::RequestLayoutState { inline_elements },
)
}

fn prepaint(
&mut self,
_global_id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
layout_state: &mut Self::RequestLayoutState,
window: &mut Window,
app: &mut App,
) -> Self::PrepaintState {
self.layout.prepaint(bounds, &self.text);

for (idx, inline_element) in layout_state.inline_elements.iter_mut().enumerate() {
let origin = self.layout.position_of_inline_box(idx);

if let Some(origin) = origin {
inline_element.prepaint_at(origin, window, app);
}
}

()
}

fn paint(
&mut self,
_global_id: Option<&GlobalElementId>,
_bounds: Bounds<Pixels>,
layout_state: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
window: &mut Window,
app: &mut App,
) {
self.layout.paint(&self.text, window, app);

for inline_element in &mut layout_state.inline_elements {
inline_element.paint(window, app);
}
}
}

impl IntoElement for InlineText {
type Element = Self;

fn into_element(self) -> Self::Element {
self
}
}
Loading