Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
33 changes: 27 additions & 6 deletions crates/warpui_core/src/elements/tui/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ enum TuiTextOverflow {
Clip,
Ellipsis,
}
#[derive(Clone, Copy)]
struct TuiTextMeasurement {
available_width: u16,
natural_size: TuiSize,
}

pub struct TuiText {
/// Styled runs that concatenate into the full text. Runs may contain hard
Expand All @@ -56,6 +61,7 @@ pub struct TuiText {
style: TuiStyle,
wrap: bool,
overflow: TuiTextOverflow,
cached_measurement: Option<TuiTextMeasurement>,
Comment thread
kevinyang372 marked this conversation as resolved.
size: Option<TuiSize>,
origin: Option<TuiScreenPoint>,
}
Expand All @@ -75,6 +81,7 @@ impl TuiText {
style: TuiStyle::default(),
wrap: true,
overflow: TuiTextOverflow::default(),
cached_measurement: None,
size: None,
origin: None,
}
Expand All @@ -88,13 +95,15 @@ impl TuiText {
/// Lays each hard line out as a single (clipped) row instead of wrapping.
pub fn truncate(mut self) -> Self {
self.wrap = false;
self.cached_measurement = None;
self
}
/// Truncates each hard line at grapheme boundaries and appends `...`
/// inside the width supplied during layout.
pub fn truncate_with_ellipsis(mut self) -> Self {
self.wrap = false;
self.overflow = TuiTextOverflow::Ellipsis;
self.cached_measurement = None;
self
}

Expand Down Expand Up @@ -262,18 +271,30 @@ impl TuiElement for TuiText {
_ctx: &mut TuiLayoutContext,
_app: &AppContext,
) -> TuiSize {
let size = if self.is_empty() {
constraint.clamp(TuiSize::ZERO)
let width = constraint.max.width;
let natural_size = if self.is_empty() {
TuiSize::ZERO
} else if let Some(measurement) = self
.cached_measurement
.filter(|measurement| measurement.available_width == width)
{
measurement.natural_size
} else {
let paragraph = self.paragraph(constraint.max.width);
let height =
u16::try_from(paragraph.line_count(constraint.max.width)).unwrap_or(u16::MAX);
let content_width = u16::try_from(paragraph.line_width()).unwrap_or(u16::MAX);
TuiSize::new(
constraint.constrain_width(content_width),
constraint.constrain_height(height),
)
let size = TuiSize::new(content_width, height);
self.cached_measurement = Some(TuiTextMeasurement {
available_width: width,
natural_size: size,
});
size
};
let size = TuiSize::new(
constraint.constrain_width(natural_size.width),
constraint.constrain_height(natural_size.height),
);
self.size = Some(size);
size
}
Expand Down
28 changes: 28 additions & 0 deletions crates/warpui_core/src/elements/tui/text_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,34 @@ fn layout_reports_content_width_and_row_count() {
});
}

#[test]
fn truncation_invalidates_a_cached_wrapped_measurement() {
App::test((), |app| async move {
app.read(|app_ctx| {
let constraint = TuiConstraint::loose(TuiSize::new(5, 10));
for truncate in [
TuiText::truncate as fn(TuiText) -> TuiText,
TuiText::truncate_with_ellipsis,
] {
let mut text = TuiText::new("hello world");
let mut rendered_views = EntityIdMap::default();
let mut ctx = TuiLayoutContext {
rendered_views: &mut rendered_views,
};
assert_eq!(
text.layout(constraint, &mut ctx, app_ctx),
TuiSize::new(5, 2)
);

text = truncate(text);
assert_eq!(
text.layout(constraint, &mut ctx, app_ctx),
TuiSize::new(5, 1)
);
}
});
});
}
#[test]
fn word_wraps_at_the_width_boundary() {
let text = TuiText::new("hello world foo");
Expand Down
11 changes: 7 additions & 4 deletions crates/warpui_core/src/elements/tui/viewported_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,12 @@ where
let Some(resolved) = self.state.resolved_viewport() else {
return;
};
let selection_is_valid = selection.validate_width(size.width);
let selection_range = selection_is_valid.then(|| selection.range()).flatten();
if selection_range.is_none() && !self.trim_selection_line_ends {
self.selection_snapshot.borrow_mut().take();
return;
}
let visible_height = size.height.saturating_sub(resolved.screen_offset).min(
resolved
.content_height
Expand All @@ -171,10 +177,7 @@ where
.collect::<Vec<_>>()
});
*self.selection_snapshot.borrow_mut() = Some((resolved, snapshot));
if !selection.validate_width(size.width) {
return;
}
let Some(range) = selection.range() else {
let Some(range) = selection_range else {
return;
};
let viewport_bottom = resolved.window.scroll_top.saturating_add(usize::from(
Expand Down
Loading