From ba42bafc3a6340eb0ad6d0daf23efaf65b5f14b8 Mon Sep 17 00:00:00 2001 From: reya Date: Mon, 19 May 2025 07:25:39 +0700 Subject: [PATCH] chore: fix input auto-grow height --- crates/coop/src/views/chat.rs | 1 + crates/ui/src/input/element.rs | 67 ++++++++++++++++++++++------------ crates/ui/src/input/state.rs | 32 ++++++---------- 3 files changed, 57 insertions(+), 43 deletions(-) diff --git a/crates/coop/src/views/chat.rs b/crates/coop/src/views/chat.rs index 1b23749..cf91182 100644 --- a/crates/coop/src/views/chat.rs +++ b/crates/coop/src/views/chat.rs @@ -78,6 +78,7 @@ impl Chat { .multi_line() .prevent_new_line_on_enter() .rows(1) + .auto_grow() .clean_on_escape() .max_rows(20) }); diff --git a/crates/ui/src/input/element.rs b/crates/ui/src/input/element.rs index 5dbf5d8..9af5203 100644 --- a/crates/ui/src/input/element.rs +++ b/crates/ui/src/input/element.rs @@ -9,9 +9,9 @@ use theme::ActiveTheme; use super::InputState; use crate::Root; -const RIGHT_MARGIN: Pixels = px(5.); -const BOTTOM_MARGIN: Pixels = px(20.); const CURSOR_THICKNESS: Pixels = px(2.); +const RIGHT_MARGIN: Pixels = px(5.); +const BOTTOM_MARGIN_ROWS: usize = 1; pub(super) struct TextElement { input: Entity, @@ -60,6 +60,13 @@ impl TextElement { let mut scroll_offset = input.scroll_handle.offset(); let mut cursor = None; + // If the input has a fixed height (Otherwise is auto-grow), we need to add a bottom margin to the input. + let bottom_margin = if input.auto_grow { + px(0.) + } else { + BOTTOM_MARGIN_ROWS * line_height + line_height + }; + // The cursor corresponds to the current cursor position in the text no only the line. let mut cursor_pos = None; let mut cursor_start = None; @@ -115,16 +122,17 @@ impl TextElement { } else { scroll_offset.x }; - scroll_offset.y = - if scroll_offset.y + cursor_pos.y > (bounds.size.height - BOTTOM_MARGIN) { - // cursor is out of bottom - bounds.size.height - BOTTOM_MARGIN - cursor_pos.y - } else if scroll_offset.y + cursor_pos.y < px(0.) { - // cursor is out of top - scroll_offset.y - cursor_pos.y - } else { - scroll_offset.y - }; + scroll_offset.y = if scroll_offset.y + cursor_pos.y + line_height + > bounds.size.height - bottom_margin + { + // cursor is out of bottom + bounds.size.height - bottom_margin - cursor_pos.y + } else if scroll_offset.y + cursor_pos.y < px(0.) { + // cursor is out of top + scroll_offset.y - cursor_pos.y + } else { + scroll_offset.y + }; if input.selection_reversed { if scroll_offset.x + cursor_start.x < px(0.) { @@ -351,20 +359,36 @@ impl Element for TextElement { cx: &mut App, ) -> (LayoutId, Self::RequestLayoutState) { let input = self.input.read(cx); + let line_height = window.line_height(); + let mut style = Style::default(); style.size.width = relative(1.).into(); + if self.input.read(cx).is_multi_line() { style.flex_grow = 1.0; if let Some(h) = input.height { style.size.height = h.into(); - style.min_size.height = window.line_height().into(); + style.min_size.height = line_height.into(); } else { + // Check to auto grow + let rows = if input.auto_grow { + let rows = (input.scroll_size.height / line_height) as usize; + let max_rows = input + .max_rows + .unwrap_or(usize::MAX) + .min(rows) + .max(input.min_rows); + rows.clamp(input.min_rows, max_rows) + } else { + input.rows + }; + style.size.height = relative(1.).into(); - style.min_size.height = (input.rows.max(1) as f32 * window.line_height()).into(); + style.min_size.height = (rows.max(1) as f32 * line_height).into(); } } else { // For single-line inputs, the minimum height should be the line height - style.size.height = window.line_height().into(); + style.size.height = line_height.into(); }; (window.request_layout(style, [], cx), ()) @@ -569,25 +593,22 @@ impl Element for TextElement { .map(|l| l.width()) .max() .unwrap_or_default(); - let height = prepaint - .lines - .iter() - .map(|l| l.size(line_height).height.0) - .sum::(); - let scroll_size = size(width, px(height)); + let height = offset_y; + let scroll_size = size(width, height); - self.input.update(cx, |input, _cx| { + self.input.update(cx, |input, cx| { input.last_layout = Some(prepaint.lines.clone()); input.last_bounds = Some(bounds); input.last_cursor_offset = Some(input.cursor_offset()); input.last_line_height = line_height; input.input_bounds = input_bounds; input.last_selected_range = Some(selected_range); + input.scroll_size = scroll_size; input .scroll_handle .set_offset(prepaint.cursor_scroll_offset); - input.scroll_size = scroll_size; + cx.notify(); }); self.paint_mouse_listeners(window, cx); diff --git a/crates/ui/src/input/state.rs b/crates/ui/src/input/state.rs index 71d2358..9cfbdcc 100644 --- a/crates/ui/src/input/state.rs +++ b/crates/ui/src/input/state.rs @@ -220,6 +220,7 @@ pub struct InputState { pub(super) rows: usize, pub(super) min_rows: usize, pub(super) max_rows: Option, + pub(super) auto_grow: bool, pub(super) pattern: Option, pub(super) validate: Validate, pub(crate) scroll_handle: ScrollHandle, @@ -284,6 +285,7 @@ impl InputState { rows: 3, min_rows: 3, max_rows: None, + auto_grow: false, height: None, last_layout: None, last_bounds: None, @@ -489,6 +491,12 @@ impl InputState { self } + /// Set the auto-grow mode for the multi-line Textarea. + pub fn auto_grow(mut self) -> Self { + self.auto_grow = true; + self + } + /// Set the text of the input field. /// /// And the selection_range will be reset to 0..0. @@ -1010,25 +1018,6 @@ impl InputState { }); } - 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(); - } - pub(super) fn clean(&mut self, window: &mut Window, cx: &mut Context) { self.replace_text("", window, cx); } @@ -1591,6 +1580,10 @@ impl EntityInputHandler for InputState { self.marked_range = None; } + /// Replace text in range. + /// + /// - If the new text is invalid, it will not be replaced. + /// - If `range_utf16` is not provided, the current selected range will be used. fn replace_text_in_range( &mut self, range_utf16: Option>, @@ -1628,7 +1621,6 @@ impl EntityInputHandler for InputState { self.update_preferred_x_offset(cx); self.update_scroll_offset(None, cx); - self.check_to_auto_grow(window, cx); cx.emit(InputEvent::Change(self.unmask_value())); cx.notify();