From 9c212a52f79b93f8064f291349ed06caa5914917 Mon Sep 17 00:00:00 2001 From: temportalflux Date: Fri, 5 Jun 2026 17:28:58 -0400 Subject: [PATCH] replace multiline with InputLayout enum --- crates/gpui_elements/src/input.rs | 2 + crates/gpui_elements/src/input/element.rs | 2 - crates/gpui_elements/src/input/layout.rs | 24 +++ crates/gpui_elements/src/input/paint.rs | 146 +++++++------- crates/gpui_elements/src/input/state.rs | 180 +++++++++--------- .../src/input/state_input_handler.rs | 22 +-- 6 files changed, 184 insertions(+), 192 deletions(-) create mode 100644 crates/gpui_elements/src/input/layout.rs diff --git a/crates/gpui_elements/src/input.rs b/crates/gpui_elements/src/input.rs index fef44ff5e8..ae67ff0e12 100644 --- a/crates/gpui_elements/src/input.rs +++ b/crates/gpui_elements/src/input.rs @@ -3,6 +3,7 @@ mod colors; mod cursor; mod element; mod history; +mod layout; mod paint; mod state; mod state_input_handler; @@ -12,4 +13,5 @@ pub use colors::*; pub(self) use cursor::*; pub use element::*; pub(self) use history::*; +pub use layout::*; pub use state::*; diff --git a/crates/gpui_elements/src/input/element.rs b/crates/gpui_elements/src/input/element.rs index f77539479b..f016ee4292 100644 --- a/crates/gpui_elements/src/input/element.rs +++ b/crates/gpui_elements/src/input/element.rs @@ -15,7 +15,6 @@ pub struct Input { pub(super) interactivity: Interactivity, pub(super) placeholder: Option, pub(super) colors: PaintColors, - pub(super) multiline: bool, } impl Input { @@ -27,7 +26,6 @@ impl Input { interactivity: Interactivity::new(), placeholder: None, colors: PaintColors::default(), - multiline: false, }; input.register_actions(); input diff --git a/crates/gpui_elements/src/input/layout.rs b/crates/gpui_elements/src/input/layout.rs new file mode 100644 index 0000000000..8280d23cff --- /dev/null +++ b/crates/gpui_elements/src/input/layout.rs @@ -0,0 +1,24 @@ +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum InputLayout { + SingleLine, + MultiLine, +} + +impl InputLayout { + pub(super) fn sanitize_content<'s>(&self, content: &'s str) -> std::borrow::Cow<'s, str> { + match self { + // Strip newlines for single-line input + Self::SingleLine => { + std::borrow::Cow::Owned(content.replace('\n', " ").replace('\r', "")) + } + Self::MultiLine => std::borrow::Cow::Borrowed(content), + } + } + + pub fn axis(&self) -> gpui::Axis { + match self { + Self::SingleLine => gpui::Axis::Horizontal, + Self::MultiLine => gpui::Axis::Vertical, + } + } +} diff --git a/crates/gpui_elements/src/input/paint.rs b/crates/gpui_elements/src/input/paint.rs index 8472a017fa..280575ba92 100644 --- a/crates/gpui_elements/src/input/paint.rs +++ b/crates/gpui_elements/src/input/paint.rs @@ -1,8 +1,8 @@ use crate::input::{Input, InputLineLayout, InputState, PaintColors}; use gpui::{ - App, Bounds, ContentMask, CursorStyle, DispatchPhase, Element, ElementId, ElementInputHandler, - Entity, FocusHandle, Focusable, GlobalElementId, Hitbox, HitboxBehavior, Hsla, - InspectorElementId, LayoutId, Length, MouseButton, MouseDownEvent, MouseMoveEvent, + Along, App, Bounds, ContentMask, CursorStyle, DispatchPhase, Element, ElementId, + ElementInputHandler, Entity, FocusHandle, Focusable, GlobalElementId, Hitbox, HitboxBehavior, + Hsla, InspectorElementId, LayoutId, Length, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, ScrollWheelEvent, SharedString, TextAlign, TextRun, TextStyle, Window, WrappedLine, fill, point, px, relative, size, }; @@ -39,7 +39,6 @@ impl Element for Input { cx: &mut App, ) -> (LayoutId, Self::RequestLayoutState) { let mut resolved_text_style = None; - let multiline = self.multiline; let layout_id = self.interactivity.request_layout( global_id, @@ -47,11 +46,12 @@ impl Element for Input { window, cx, |element_style, window, cx| { + let layout = self.input.read(cx).get_layout(); window.with_text_style(element_style.text_style().cloned(), |window| { resolved_text_style = Some(window.text_style()); let mut layout_style = element_style.clone(); - if multiline { + if matches!(layout, super::InputLayout::MultiLine) { if let Length::Auto = layout_style.size.width { layout_style.size.width = relative(1.).into(); } @@ -85,10 +85,9 @@ impl Element for Input { .text_style .line_height_in_pixels(window.rem_size()); - let wrap_width = if self.multiline { - bounds.size.width - } else { - px(100000.) + let wrap_width = match self.input.read(cx).get_layout() { + super::InputLayout::SingleLine => px(100000.), + super::InputLayout::MultiLine => bounds.size.width, }; self.input.update(cx, |input, _cx| { @@ -137,7 +136,7 @@ impl Element for Input { let input = self.input.clone(); let placeholder = self.placeholder.clone(); let text_style = layout_state.text_style.clone(); - let multiline = self.multiline; + let layout = input.read(cx).get_layout(); let is_focused = focus_handle.is_focused(window); let cursor_visible = self .input @@ -152,34 +151,31 @@ impl Element for Input { window, cx, |_style, window, cx| { - handle_mouse(&input, bounds, multiline, window, cx); + handle_mouse(&input, bounds, layout.axis(), window, cx); - window.with_content_mask(Some(ContentMask { bounds }), |window| { - if multiline { - paint_multiline( - &input, - &focus_handle, - bounds, - &text_style, - placeholder.as_ref(), - &colors, - cursor_visible, - window, - cx, - ); - } else { - paint_singleline( - &input, - &focus_handle, - bounds, - &text_style, - placeholder.as_ref(), - &colors, - cursor_visible, - window, - cx, - ); - } + window.with_content_mask(Some(ContentMask { bounds }), |window| match layout { + super::InputLayout::SingleLine => paint_singleline( + &input, + &focus_handle, + bounds, + &text_style, + placeholder.as_ref(), + &colors, + cursor_visible, + window, + cx, + ), + super::InputLayout::MultiLine => paint_multiline( + &input, + &focus_handle, + bounds, + &text_style, + placeholder.as_ref(), + &colors, + cursor_visible, + window, + cx, + ), }); }, ); @@ -190,20 +186,20 @@ impl Element for Input { fn handle_mouse( input: &Entity, bounds: Bounds, - multiline: bool, + axis: gpui::Axis, window: &mut Window, cx: &App, ) { - mouse_down(input.clone(), bounds, multiline, window); + mouse_down(input.clone(), bounds, axis, window); mouse_up(input.clone(), window); - mouse_move(input.clone(), bounds, multiline, window); - handle_scroll(input.clone(), bounds, multiline, window, cx); + mouse_move(input.clone(), bounds, axis, window); + handle_scroll(input.clone(), bounds, axis, window, cx); } fn mouse_down( input: Entity, bounds: Bounds, - multiline: bool, + axis: gpui::Axis, window: &mut Window, ) { window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| { @@ -219,7 +215,7 @@ fn mouse_down( input.update(cx, |input, cx| { let text_position = - screen_to_text_position(event.position, bounds, input.scroll_offset, multiline); + screen_to_text_position(event.position, bounds, input.scroll_offset, axis); input.on_mouse_down( text_position, event.click_count, @@ -249,7 +245,7 @@ fn mouse_up(input: Entity, window: &mut Window) { fn mouse_move( input: Entity, bounds: Bounds, - multiline: bool, + axis: gpui::Axis, window: &mut Window, ) { window.on_mouse_event(move |event: &MouseMoveEvent, phase, _window, cx| { @@ -259,7 +255,7 @@ fn mouse_move( input.update(cx, |input, cx| { let text_position = - screen_to_text_position(event.position, bounds, input.scroll_offset, multiline); + screen_to_text_position(event.position, bounds, input.scroll_offset, axis); input.on_mouse_move(text_position, cx); }); }); @@ -268,23 +264,20 @@ fn mouse_move( fn handle_scroll( input: Entity, bounds: Bounds, - multiline: bool, + axis: gpui::Axis, window: &mut Window, cx: &App, ) { - let max_scroll = if multiline { - let total_height = input.read(cx).total_content_height(); - (total_height - bounds.size.height).max(px(0.)) - } else { - let text_width = input - .read(cx) - .line_layouts - .first() - .and_then(|l| l.wrapped_line.as_ref()) - .map(|w| w.width()) - .unwrap_or(px(0.)); - (text_width - bounds.size.width).max(px(0.)) + let content_size = match axis { + gpui::Axis::Horizontal => { + let state = input.read(cx); + let line = state.line_layouts.first(); + let line = line.and_then(|l| l.wrapped_line.as_ref()); + line.map(|w| w.width()).unwrap_or(px(0.)) + } + gpui::Axis::Vertical => input.read(cx).total_content_height(), }; + let max_scroll = (content_size - bounds.size.along(axis)).max(px(0.)); window.on_mouse_event(move |event: &ScrollWheelEvent, phase, _window, cx| { if phase != DispatchPhase::Bubble { @@ -296,17 +289,17 @@ fn handle_scroll( let pixel_delta = event.delta.pixel_delta(px(20.)); input.update(cx, |input, cx| { - if multiline { - input.scroll_offset = - (input.scroll_offset - pixel_delta.y).clamp(px(0.), max_scroll); - } else { - let delta = if pixel_delta.x.abs() > pixel_delta.y.abs() { - pixel_delta.x - } else { - pixel_delta.y - }; - input.scroll_offset = (input.scroll_offset - delta).clamp(px(0.), max_scroll); - } + let delta = match axis { + gpui::Axis::Horizontal => pixel_delta.y, + gpui::Axis::Vertical => { + if pixel_delta.x.abs() > pixel_delta.y.abs() { + pixel_delta.x + } else { + pixel_delta.y + } + } + }; + input.scroll_offset = (input.scroll_offset - delta).clamp(px(0.), max_scroll); cx.notify(); }); }); @@ -318,19 +311,10 @@ fn screen_to_text_position( screen_position: Point, bounds: Bounds, scroll_offset: Pixels, - multiline: bool, + axis: gpui::Axis, ) -> Point { - if multiline { - point( - screen_position.x - bounds.origin.x, - screen_position.y - bounds.origin.y + scroll_offset, - ) - } else { - point( - screen_position.x - bounds.origin.x + scroll_offset, - screen_position.y - bounds.origin.y, - ) - } + let point = screen_position - bounds.origin; + point.apply_along(axis, |pos| pos + scroll_offset) } fn paint_multiline( diff --git a/crates/gpui_elements/src/input/state.rs b/crates/gpui_elements/src/input/state.rs index d2475e3b52..c7767449e4 100644 --- a/crates/gpui_elements/src/input/state.rs +++ b/crates/gpui_elements/src/input/state.rs @@ -1,5 +1,5 @@ use super::actions::*; -use crate::input::unicode::UnicodeString; +use crate::input::{InputLayout, unicode::UnicodeString}; use gpui::{ App, AppContext, ClipboardItem, Context, Entity, EntityId, EntityInputHandler, EventEmitter, FocusHandle, Focusable, Pixels, Point, SharedString, Subscription, TextRun, TextStyle, Window, @@ -54,7 +54,7 @@ pub struct InputState { pub(super) scroll_offset: Pixels, pub(super) available_height: Pixels, pub(super) available_width: Pixels, - pub(super) multiline: bool, + pub(super) layout: InputLayout, /// Stack of previous states for undo. undo_stack: Vec, /// Stack of undone states for redo. @@ -121,7 +121,7 @@ impl InputState { scroll_offset: px(0.), available_height: px(0.), available_width: px(0.), - multiline: false, + layout: InputLayout::SingleLine, undo_stack: Vec::new(), cached_utf16_len: None, redo_stack: Vec::new(), @@ -201,16 +201,14 @@ impl InputState { &mut self.content } + pub fn get_layout(&self) -> InputLayout { + self.layout + } + /// Sets the text content, resetting selection to the beginning. /// This clears the undo/redo history. - pub fn set_content(&mut self, content: impl Into, cx: &mut Context) { - let content = content.into(); - self.content = if self.multiline { - content - } else { - // Strip newlines for single-line input - content.replace('\n', " ").replace('\r', "") - }; + pub fn set_content(&mut self, content: impl AsRef, cx: &mut Context) { + self.content = self.layout.sanitize_content(content.as_ref()).to_string(); self.selected_range = 0..0; self.selection_reversed = false; self.marked_range = None; @@ -375,13 +373,7 @@ impl InputState { .unwrap_or(self.selected_range.clone()); let range = range.start.min(self.content.len())..range.end.min(self.content.len()); - let sanitized_text; - let text_to_insert = if self.multiline { - text - } else { - sanitized_text = text.replace('\n', " ").replace('\r', ""); - &sanitized_text - }; + let text_to_insert = self.layout.sanitize_content(text); // Record patch for undo before modifying content self.push_undo_patch(range.clone(), text_to_insert.len()); @@ -396,7 +388,7 @@ impl InputState { self.cached_utf16_len = Some(cached_len - removed_utf16_len + added_utf16_len); } - self.content.replace_range(range.clone(), text_to_insert); + self.content.replace_range(range.clone(), &text_to_insert); self.selected_range = range.start + text_to_insert.len()..range.start + text_to_insert.len(); self.marked_range.take(); @@ -478,38 +470,44 @@ impl InputState { pub(crate) fn up(&mut self, _: &Up, _window: &mut Window, cx: &mut Context) { self.pause_cursor_blink(cx); - if !self.multiline { - // In single-line mode, up moves to start - self.selected_range = 0..0; - self.selection_reversed = false; - self.scroll_to_cursor(); - cx.notify(); - return; - } - if let Some(new_offset) = self.move_vertically(self.cursor_offset(), -1) { - self.selected_range = new_offset..new_offset; - self.selection_reversed = false; - self.scroll_to_cursor(); - cx.notify(); + match self.layout { + InputLayout::SingleLine => { + // In single-line mode, up moves to start + self.selected_range = 0..0; + self.selection_reversed = false; + self.scroll_to_cursor(); + cx.notify(); + } + InputLayout::MultiLine => { + if let Some(new_offset) = self.move_vertically(self.cursor_offset(), -1) { + self.selected_range = new_offset..new_offset; + self.selection_reversed = false; + self.scroll_to_cursor(); + cx.notify(); + } + } } } pub(crate) fn down(&mut self, _: &Down, _window: &mut Window, cx: &mut Context) { self.pause_cursor_blink(cx); - if !self.multiline { - // In single-line mode, down moves to end - let end = self.content.len(); - self.selected_range = end..end; - self.selection_reversed = false; - self.scroll_to_cursor(); - cx.notify(); - return; - } - if let Some(new_offset) = self.move_vertically(self.cursor_offset(), 1) { - self.selected_range = new_offset..new_offset; - self.selection_reversed = false; - self.scroll_to_cursor(); - cx.notify(); + match self.layout { + InputLayout::SingleLine => { + // In single-line mode, down moves to end + let end = self.content.len(); + self.selected_range = end..end; + self.selection_reversed = false; + self.scroll_to_cursor(); + cx.notify(); + } + InputLayout::MultiLine => { + if let Some(new_offset) = self.move_vertically(self.cursor_offset(), 1) { + self.selected_range = new_offset..new_offset; + self.selection_reversed = false; + self.scroll_to_cursor(); + cx.notify(); + } + } } } @@ -523,23 +521,26 @@ impl InputState { pub(crate) fn select_up(&mut self, _: &SelectUp, _window: &mut Window, cx: &mut Context) { self.pause_cursor_blink(cx); - if !self.multiline { - // In single-line mode, select_up selects to start - self.select_to(0, cx); - return; - } - if let Some(new_offset) = self.move_vertically(self.cursor_offset(), -1) { - if self.selection_reversed { - self.selected_range.start = new_offset; - } else { - self.selected_range.end = new_offset; + match self.layout { + InputLayout::SingleLine => { + // In single-line mode, select_up selects to start + self.select_to(0, cx); } - if self.selected_range.end < self.selected_range.start { - self.selection_reversed = !self.selection_reversed; - self.selected_range = self.selected_range.end..self.selected_range.start; + InputLayout::MultiLine => { + if let Some(new_offset) = self.move_vertically(self.cursor_offset(), -1) { + if self.selection_reversed { + self.selected_range.start = new_offset; + } else { + self.selected_range.end = new_offset; + } + if self.selected_range.end < self.selected_range.start { + self.selection_reversed = !self.selection_reversed; + self.selected_range = self.selected_range.end..self.selected_range.start; + } + self.scroll_to_cursor(); + cx.notify(); + } } - self.scroll_to_cursor(); - cx.notify(); } } @@ -550,23 +551,26 @@ impl InputState { cx: &mut Context, ) { self.pause_cursor_blink(cx); - if !self.multiline { - // In single-line mode, select_down selects to end - self.select_to(self.content.len(), cx); - return; - } - if let Some(new_offset) = self.move_vertically(self.cursor_offset(), 1) { - if self.selection_reversed { - self.selected_range.start = new_offset; - } else { - self.selected_range.end = new_offset; + match self.layout { + InputLayout::SingleLine => { + // In single-line mode, select_down selects to end + self.select_to(self.content.len(), cx); } - if self.selected_range.end < self.selected_range.start { - self.selection_reversed = !self.selection_reversed; - self.selected_range = self.selected_range.end..self.selected_range.start; + InputLayout::MultiLine => { + if let Some(new_offset) = self.move_vertically(self.cursor_offset(), 1) { + if self.selection_reversed { + self.selected_range.start = new_offset; + } else { + self.selected_range.end = new_offset; + } + if self.selected_range.end < self.selected_range.start { + self.selection_reversed = !self.selection_reversed; + self.selected_range = self.selected_range.end..self.selected_range.start; + } + self.scroll_to_cursor(); + cx.notify(); + } } - self.scroll_to_cursor(); - cx.notify(); } } @@ -642,7 +646,7 @@ impl InputState { } pub(crate) fn enter(&mut self, _: &Enter, window: &mut Window, cx: &mut Context) { - if self.multiline { + if matches!(&self.layout, InputLayout::MultiLine) { self.replace_text_in_range(None, "\n", window, cx); } } @@ -714,15 +718,11 @@ impl InputState { } pub(crate) fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context) { - if let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) { - if self.multiline { - self.replace_text_in_range(None, &text, window, cx); - } else { - // Strip newlines for single-line input - let text = text.replace('\n', " ").replace('\r', ""); - self.replace_text_in_range(None, &text, window, cx); - } - } + let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) else { + return; + }; + let text = self.layout.sanitize_content(&text); + self.replace_text_in_range(None, &text, window, cx); } pub(crate) fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context) { @@ -986,11 +986,9 @@ impl InputState { } let cursor_offset = self.cursor_offset(); - - if self.multiline { - self.scroll_to_cursor_vertical(cursor_offset); - } else { - self.scroll_to_cursor_horizontal(cursor_offset); + match self.layout { + InputLayout::SingleLine => self.scroll_to_cursor_horizontal(cursor_offset), + InputLayout::MultiLine => self.scroll_to_cursor_vertical(cursor_offset), } } diff --git a/crates/gpui_elements/src/input/state_input_handler.rs b/crates/gpui_elements/src/input/state_input_handler.rs index 94274ebfd3..2a88a42e27 100644 --- a/crates/gpui_elements/src/input/state_input_handler.rs +++ b/crates/gpui_elements/src/input/state_input_handler.rs @@ -59,14 +59,7 @@ impl EntityInputHandler for super::InputState { let range = range.start.min(self.content().len())..range.end.min(self.content().len()); - // Strip newlines for single-line input - let sanitized_text; - let text_to_insert = if self.multiline { - new_text - } else { - sanitized_text = new_text.replace('\n', " ").replace('\r', ""); - &sanitized_text - }; + let text_to_insert = self.layout.sanitize_content(new_text); // Record patch for undo before modifying content self.push_undo_patch(range.clone(), text_to_insert.len()); @@ -82,7 +75,7 @@ impl EntityInputHandler for super::InputState { } self.content_mut() - .replace_range(range.clone(), text_to_insert); + .replace_range(range.clone(), &text_to_insert); self.selected_range = range.start + text_to_insert.len()..range.start + text_to_insert.len(); self.marked_range.take(); @@ -108,14 +101,7 @@ impl EntityInputHandler for super::InputState { let range = range.start.min(self.content().len())..range.end.min(self.content().len()); - // Strip newlines for single-line input - let sanitized_text; - let text_to_insert = if self.multiline { - new_text - } else { - sanitized_text = new_text.replace('\n', " ").replace('\r', ""); - &sanitized_text - }; + let text_to_insert = self.layout.sanitize_content(new_text); // Update cached UTF-16 length incrementally if available if let Some(cached_len) = self.cached_utf16_len { @@ -128,7 +114,7 @@ impl EntityInputHandler for super::InputState { } self.content_mut() - .replace_range(range.clone(), text_to_insert); + .replace_range(range.clone(), &text_to_insert); if !text_to_insert.is_empty() { self.marked_range = Some(range.start..range.start + text_to_insert.len());