input: Add max_rows to support auto-grow. (#768)

Closes #762


https://github.com/user-attachments/assets/068a3f11-8498-4db6-a2ac-00cc043f16cf
This commit is contained in:
Jason Lee 2025-04-02 15:54:19 +08:00 committed by GitHub
parent 253c93063f
commit c91dce3139
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 65 additions and 24 deletions

View file

@ -63,7 +63,8 @@ impl FormStory {
let bio_input = cx.new(|cx| {
let mut input = TextInput::new(window, cx)
.multi_line()
.rows(10)
.rows(5)
.max_rows(20)
.placeholder("Enter text here...");
input.set_text("Hello 世界this is GPUI component.", window, cx);
input

View file

@ -1,7 +1,7 @@
use gpui::{
actions, div, prelude::FluentBuilder as _, px, App, AppContext as _, ClickEvent, Context,
Entity, FocusHandle, Focusable, InteractiveElement, IntoElement, KeyBinding,
ParentElement as _, Render, SharedString, Styled, Window,
ParentElement as _, Render, SharedString, Styled, Subscription, Window,
};
use regex::Regex;
@ -46,6 +46,8 @@ pub struct InputStory {
otp_input_small: Entity<OtpInput>,
otp_input_large: Entity<OtpInput>,
opt_input_sized: Entity<OtpInput>,
_subscriptions: Vec<Subscription>,
}
impl super::Story for InputStory {
@ -77,12 +79,8 @@ impl InputStory {
);
input
});
cx.subscribe_in(&input1, window, Self::on_input_event)
.detach();
let input2 = cx.new(|cx| TextInput::new(window, cx).placeholder("Enter text here..."));
cx.subscribe_in(&input2, window, Self::on_input_event)
.detach();
let textarea = cx.new(|cx| {
let mut input = TextInput::new(window, cx)
@ -115,8 +113,6 @@ impl InputStory {
);
input
});
cx.subscribe_in(&textarea, window, Self::on_input_event)
.detach();
let number_input1_value = 1;
let number_input1 = cx.new(|cx| {
@ -124,8 +120,6 @@ impl InputStory {
input.set_value(number_input1_value.to_string(), window, cx);
input
});
cx.subscribe_in(&number_input1, window, Self::on_number_input1_event)
.detach();
let number_input2 = cx.new(|cx| {
NumberInput::new(window, cx)
@ -134,9 +128,6 @@ impl InputStory {
.small()
});
cx.subscribe_in(&number_input2, window, Self::on_number_input2_event)
.detach();
let mask_input = cx.new(|cx| {
let mut input = TextInput::new(window, cx)
.masked(true)
@ -168,14 +159,21 @@ impl InputStory {
});
let otp_input = cx.new(|cx| OtpInput::new(6, window, cx).masked(true));
cx.subscribe(&otp_input, |this, _, ev: &InputEvent, cx| match ev {
InputEvent::Change(text) => {
this.otp_value = Some(text.clone());
cx.notify();
}
_ => {}
})
.detach();
let _subscriptions = vec![
cx.subscribe_in(&input1, window, Self::on_input_event),
cx.subscribe_in(&input2, window, Self::on_input_event),
cx.subscribe_in(&textarea, window, Self::on_input_event),
cx.subscribe_in(&number_input1, window, Self::on_number_input1_event),
cx.subscribe_in(&number_input2, window, Self::on_number_input2_event),
cx.subscribe(&otp_input, |this, _, ev: &InputEvent, cx| match ev {
InputEvent::Change(text) => {
this.otp_value = Some(text.clone());
cx.notify();
}
_ => {}
}),
];
Self {
input1,
@ -230,6 +228,7 @@ impl InputStory {
.default_value("654321")
.with_size(px(55.))
}),
_subscriptions,
}
}

View file

@ -226,6 +226,8 @@ pub struct TextInput {
pub(super) cleanable: bool,
pub(super) size: Size,
pub(super) rows: usize,
pub(super) min_rows: usize,
pub(super) max_rows: Option<usize>,
/// For special case, e.g.: NumberInput + - button
pub(super) no_gap: bool,
pub(super) height: Option<gpui::DefiniteLength>,
@ -293,6 +295,8 @@ impl TextInput {
pattern: None,
validate: None,
rows: 2,
min_rows: 2,
max_rows: None,
last_layout: None,
last_bounds: None,
last_selected_range: None,
@ -456,6 +460,19 @@ impl TextInput {
/// default: 2
pub fn rows(mut self, rows: usize) -> Self {
self.rows = rows;
self.min_rows = rows;
self
}
/// Set the maximum number of rows for the multi-line Textarea.
///
/// If max_rows is more than rows, then will enable auto-grow.
///
/// This is only used when `multi_line` is set to true.
///
/// default: None
pub fn max_rows(mut self, max_rows: usize) -> Self {
self.max_rows = Some(max_rows);
self
}
@ -1003,6 +1020,23 @@ impl TextInput {
cx.emit(InputEvent::PressEnter);
}
fn check_to_auto_grow(&mut self, _: &mut Window, cx: &mut Context<Self>) {
if !self.is_multi_line() {
return;
}
let Some(max_rows) = self.max_rows else {
return;
};
let changed_rows = ((self.scroll_size.height - self.input_bounds.size.height)
/ self.last_line_height) as isize;
self.rows = (self.rows as isize + changed_rows)
.clamp(self.min_rows as isize, max_rows as isize)
.max(0) as usize;
cx.notify();
}
fn clean(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
self.replace_text("", window, cx);
}
@ -1037,19 +1071,24 @@ impl TextInput {
&mut self,
event: &ScrollWheelEvent,
_window: &mut Window,
_cx: &mut Context<Self>,
cx: &mut Context<Self>,
) {
let delta = event.delta.pixel_delta(self.last_line_height);
self.update_scroll_offset(Some(self.scroll_handle.offset() + delta), cx);
}
fn update_scroll_offset(&mut self, offset: Option<Point<Pixels>>, cx: &mut Context<Self>) {
let mut offset = offset.unwrap_or(self.scroll_handle.offset());
let safe_y_range =
(-self.scroll_size.height + self.input_bounds.size.height).min(px(0.0))..px(0.);
let safe_x_range =
(-self.scroll_size.width + self.input_bounds.size.width).min(px(0.0))..px(0.);
let mut offset = self.scroll_handle.offset() + delta;
offset.y = offset.y.clamp(safe_y_range.start, safe_y_range.end);
offset.x = offset.x.clamp(safe_x_range.start, safe_x_range.end);
self.scroll_handle.set_offset(offset);
cx.notify();
}
fn show_character_palette(
@ -1576,6 +1615,8 @@ impl EntityInputHandler for TextInput {
self.selected_range = range.start + new_text.len()..range.start + new_text.len();
self.marked_range.take();
self.update_preferred_x_offset(cx);
self.update_scroll_offset(None, cx);
self.check_to_auto_grow(window, cx);
cx.emit(InputEvent::Change(self.text.clone()));
cx.notify();
}