From c91dce313970d72508d3c5784ba4e66f18eb027c Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Wed, 2 Apr 2025 15:54:19 +0800 Subject: [PATCH] input: Add max_rows to support auto-grow. (#768) Closes #762 https://github.com/user-attachments/assets/068a3f11-8498-4db6-a2ac-00cc043f16cf --- crates/story/src/form_story.rs | 3 ++- crates/story/src/input_story.rs | 39 +++++++++++++-------------- crates/ui/src/input/input.rs | 47 ++++++++++++++++++++++++++++++--- 3 files changed, 65 insertions(+), 24 deletions(-) diff --git a/crates/story/src/form_story.rs b/crates/story/src/form_story.rs index 35a6139d..92743877 100644 --- a/crates/story/src/form_story.rs +++ b/crates/story/src/form_story.rs @@ -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 diff --git a/crates/story/src/input_story.rs b/crates/story/src/input_story.rs index 49072fd9..ddeabe97 100644 --- a/crates/story/src/input_story.rs +++ b/crates/story/src/input_story.rs @@ -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, otp_input_large: Entity, opt_input_sized: Entity, + + _subscriptions: Vec, } 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, } } diff --git a/crates/ui/src/input/input.rs b/crates/ui/src/input/input.rs index 19937512..12b38f5a 100644 --- a/crates/ui/src/input/input.rs +++ b/crates/ui/src/input/input.rs @@ -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, /// For special case, e.g.: NumberInput + - button pub(super) no_gap: bool, pub(super) height: Option, @@ -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) { + 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.replace_text("", window, cx); } @@ -1037,19 +1071,24 @@ impl TextInput { &mut self, event: &ScrollWheelEvent, _window: &mut Window, - _cx: &mut Context, + cx: &mut Context, ) { 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>, cx: &mut Context) { + 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(); }