From ca548bd8568eb075750c1d931333dd39d13af338 Mon Sep 17 00:00:00 2001
From: iincity <56342737@qq.com>
Date: Mon, 1 Sep 2025 12:33:21 +0800
Subject: [PATCH] input: Add to support horizontal scrolling to multi-line
text. (#1131)
- Add `soft_wrap` option to build InputState, default true.
---------
Co-authored-by: Jason Lee
---
crates/story/src/textarea_story.rs | 21 ++++++++++++++-------
crates/ui/src/input/element.rs | 2 +-
crates/ui/src/input/state.rs | 23 ++++++++++++++++++++++-
crates/ui/src/input/text_input.rs | 11 +++++++----
crates/ui/src/input/text_wrapper.rs | 13 ++++++++-----
5 files changed, 52 insertions(+), 18 deletions(-)
diff --git a/crates/story/src/textarea_story.rs b/crates/story/src/textarea_story.rs
index 5889920a..54126cd3 100644
--- a/crates/story/src/textarea_story.rs
+++ b/crates/story/src/textarea_story.rs
@@ -23,6 +23,7 @@ pub fn init(cx: &mut App) {
pub struct TextareaStory {
textarea: Entity,
textarea_auto_grow: Entity,
+ textarea_no_wrap: Entity,
}
impl super::Story for TextareaStory {
@@ -84,9 +85,18 @@ impl TextareaStory {
.default_value("Hello 世界,this is GPUI component.")
});
+ let textarea_no_wrap = cx.new(|cx| {
+ InputState::new(window, cx)
+ .multi_line()
+ .rows(6)
+ .soft_wrap(false)
+ .default_value("This is a very long line of text to test if the horizontal scrolling function is working properly, and it should not wrap automatically but display a horizontal scrollbar.\nThe second line is also very long text, used to test the horizontal scrolling effect under multiple lines, and you can input more content to test.\nThe third line: Here you can input other long text content that requires horizontal scrolling.\n")
+ });
+
Self {
textarea,
textarea_auto_grow,
+ textarea_no_wrap,
}
}
@@ -141,8 +151,6 @@ impl Render for TextareaStory {
.id("textarea-story")
.on_action(cx.listener(Self::tab))
.on_action(cx.listener(Self::tab_prev))
- .size_full()
- .justify_start()
.gap_3()
.child(
section("Textarea").child(
@@ -179,12 +187,11 @@ impl Render for TextareaStory {
),
),
)
+ .child(section("Textarea Auto Grow").child(TextInput::new(&self.textarea_auto_grow)))
.child(
- section("Textarea Auto Grow").child(
- v_flex()
- .w_full()
- .child(TextInput::new(&self.textarea_auto_grow)),
- ),
+ section("No Wrap")
+ .max_w_md()
+ .child(TextInput::new(&self.textarea_no_wrap).h(px(200.))),
)
}
}
diff --git a/crates/ui/src/input/element.rs b/crates/ui/src/input/element.rs
index e11d7b2f..4e4e6bf7 100644
--- a/crates/ui/src/input/element.rs
+++ b/crates/ui/src/input/element.rs
@@ -660,7 +660,7 @@ impl Element for TextElement {
vec![run]
};
- let wrap_width = if multi_line {
+ let wrap_width = if multi_line && state.soft_wrap {
Some(bounds.size.width - line_number_width)
} else {
None
diff --git a/crates/ui/src/input/state.rs b/crates/ui/src/input/state.rs
index 22100f4b..86702de9 100644
--- a/crates/ui/src/input/state.rs
+++ b/crates/ui/src/input/state.rs
@@ -266,6 +266,7 @@ pub struct InputState {
pub(super) disabled: bool,
pub(super) masked: bool,
pub(super) clean_on_escape: bool,
+ pub(super) soft_wrap: bool,
pub(super) pattern: Option,
pub(super) validate: Option) -> bool + 'static>>,
pub(crate) scroll_handle: ScrollHandle,
@@ -335,6 +336,7 @@ impl InputState {
disabled: false,
masked: false,
clean_on_escape: false,
+ soft_wrap: true,
loading: false,
pattern: None,
validate: None,
@@ -751,6 +753,18 @@ impl InputState {
self
}
+ /// Set the soft wrap mode for multi-line input, default is true.
+ pub fn soft_wrap(mut self, wrap: bool) -> Self {
+ self.soft_wrap = wrap;
+ self
+ }
+
+ /// Update the soft wrap mode for multi-line input, default is true.
+ pub fn set_soft_wrap(&mut self, wrap: bool, _: &mut Window, cx: &mut Context) {
+ self.soft_wrap = wrap;
+ cx.notify();
+ }
+
/// Set the regular expression pattern of the input field.
pub fn pattern(mut self, pattern: regex::Regex) -> Self {
self.pattern = Some(pattern);
@@ -2100,7 +2114,14 @@ impl InputState {
// Update text_wrapper wrap_width if changed.
if let Some(last_layout) = self.last_layout.as_ref() {
if wrap_width_changed {
- self.text_wrapper.set_wrap_width(last_layout.wrap_width, cx);
+ let wrap_width = if !self.soft_wrap {
+ // None to disable wrapping (will use Pixels::MAX)
+ None
+ } else {
+ last_layout.wrap_width
+ };
+
+ self.text_wrapper.set_wrap_width(wrap_width, cx);
self.mode.update_auto_grow(&self.text_wrapper);
cx.notify();
}
diff --git a/crates/ui/src/input/text_input.rs b/crates/ui/src/input/text_input.rs
index 3d99b427..7c7c8e1e 100644
--- a/crates/ui/src/input/text_input.rs
+++ b/crates/ui/src/input/text_input.rs
@@ -301,6 +301,12 @@ impl RenderOnce for TextInput {
.refine_style(&self.style)
.when(state.mode.is_multi_line(), |this| {
if state.last_layout.is_some() {
+ let scrollbar = if !state.soft_wrap {
+ Scrollbar::both(&state.scroll_state, &state.scroll_handle)
+ } else {
+ Scrollbar::vertical(&state.scroll_state, &state.scroll_handle)
+ };
+
this.relative().child(
div()
.absolute()
@@ -308,10 +314,7 @@ impl RenderOnce for TextInput {
.left_0()
.right(px(1.))
.bottom_0()
- .child(
- Scrollbar::vertical(&state.scroll_state, &state.scroll_handle)
- .scroll_size(state.scroll_size),
- ),
+ .child(scrollbar.scroll_size(state.scroll_size)),
)
} else {
this
diff --git a/crates/ui/src/input/text_wrapper.rs b/crates/ui/src/input/text_wrapper.rs
index b99294dd..d30f3f94 100644
--- a/crates/ui/src/input/text_wrapper.rs
+++ b/crates/ui/src/input/text_wrapper.rs
@@ -64,7 +64,7 @@ impl TextWrapper {
let mut wrapped_lines = vec![];
let mut lines = vec![];
- let wrap_width = self.wrap_width.unwrap_or(Pixels::MAX);
+ let wrap_width = self.wrap_width;
let mut line_wrapper = cx
.text_system()
.line_wrapper(self.font.clone(), self.font_size);
@@ -73,10 +73,13 @@ impl TextWrapper {
let mut line_wraps = vec![];
let mut prev_boundary_ix = 0;
- // Here only have wrapped line, if there is no wrap meet, the `line_wraps` result will empty.
- for boundary in line_wrapper.wrap_line(&[LineFragment::text(line)], wrap_width) {
- line_wraps.push(prev_boundary_ix..boundary.ix);
- prev_boundary_ix = boundary.ix;
+ // If wrap_width is Pixels::MAX, skip wrapping to disable word wrap
+ if let Some(wrap_width) = wrap_width {
+ // Here only have wrapped line, if there is no wrap meet, the `line_wraps` result will empty.
+ for boundary in line_wrapper.wrap_line(&[LineFragment::text(line)], wrap_width) {
+ line_wraps.push(prev_boundary_ix..boundary.ix);
+ prev_boundary_ix = boundary.ix;
+ }
}
lines.push(LineWrap {