input: Add to support CodeEditor as single line mode. (#1696)
## Screenshot From `input_story`: <img width="1069" height="122" alt="Screenshot From 2025-11-28 09-14-03" src="https://github.com/user-attachments/assets/f14ef2fc-4939-4ceb-be3e-bede492f8602" /> ## Breaking Changes - `.multi_line()` was changed to `.multi_line(bool)`. - Removed pub `InputMode`, this should only for internal. ```diff InputState::new(window, cx) - .multi_line() + .multi_line(true) ``` ## Checklist - [x] I have read the [CONTRIBUTING](../CONTRIBUTING.md) document and followed the guidelines. - [x] Reviewed the changes in this PR and confirmed AI generated code (If any) is accurate. - [x] Passed `cargo run` for story tests related to the changes. - [ ] Tested macOS, Windows and Linux platforms performance (if the change is platform-specific) Use cases, 1) In my API client, I have a tree sitter language for the URL input, <img width="1001" height="162" alt="Image" src="https://github.com/user-attachments/assets/9771decf-b0b7-4230-8ed6-784a95b72af1" /> 2) In my SQL editor I want to be able to allow JSON syntax highlighting for inline editing of json columns, <img width="501" height="130" alt="Image" src="https://github.com/user-attachments/assets/f0f879cc-841f-49d2-a23a-2effc3747f38" /> --------- Co-authored-by: Jason Lee <huacnlee@gmail.com>
This commit is contained in:
parent
1ce09cc1d1
commit
7e479aa7f2
8 changed files with 302 additions and 240 deletions
|
|
@ -22,7 +22,7 @@ impl Example {
|
||||||
|
|
||||||
let editor = cx.new(|cx| {
|
let editor = cx.new(|cx| {
|
||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
.multi_line()
|
.multi_line(true)
|
||||||
.tab_size(TabSize {
|
.tab_size(TabSize {
|
||||||
tab_size: 4,
|
tab_size: 4,
|
||||||
hard_tabs: false,
|
hard_tabs: false,
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ pub struct InputStory {
|
||||||
mask_input2: Entity<InputState>,
|
mask_input2: Entity<InputState>,
|
||||||
currency_input: Entity<InputState>,
|
currency_input: Entity<InputState>,
|
||||||
custom_input: Entity<InputState>,
|
custom_input: Entity<InputState>,
|
||||||
|
code_input: Entity<InputState>,
|
||||||
|
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
}
|
}
|
||||||
|
|
@ -87,6 +88,13 @@ impl InputStory {
|
||||||
let custom_input =
|
let custom_input =
|
||||||
cx.new(|cx| InputState::new(window, cx).placeholder("here is a custom input"));
|
cx.new(|cx| InputState::new(window, cx).placeholder("here is a custom input"));
|
||||||
|
|
||||||
|
let code_input = cx.new(|cx| {
|
||||||
|
InputState::new(window, cx)
|
||||||
|
.code_editor("json")
|
||||||
|
.multi_line(false)
|
||||||
|
.default_value(r#"{"single_line":"code editor"}"#)
|
||||||
|
});
|
||||||
|
|
||||||
let _subscriptions = vec![
|
let _subscriptions = vec![
|
||||||
cx.subscribe_in(&input1, window, Self::on_input_event),
|
cx.subscribe_in(&input1, window, Self::on_input_event),
|
||||||
cx.subscribe_in(&input2, window, Self::on_input_event),
|
cx.subscribe_in(&input2, window, Self::on_input_event),
|
||||||
|
|
@ -113,6 +121,7 @@ impl InputStory {
|
||||||
mask_input2,
|
mask_input2,
|
||||||
currency_input,
|
currency_input,
|
||||||
custom_input,
|
custom_input,
|
||||||
|
code_input,
|
||||||
_subscriptions,
|
_subscriptions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -250,5 +259,10 @@ impl Render for InputStory {
|
||||||
.child(Input::new(&self.custom_input).appearance(false)),
|
.child(Input::new(&self.custom_input).appearance(false)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.child(
|
||||||
|
section("Single line code editor")
|
||||||
|
.max_w_md()
|
||||||
|
.child(Input::new(&self.code_input)),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ impl TextareaStory {
|
||||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let textarea = cx.new(|cx| {
|
let textarea = cx.new(|cx| {
|
||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
.multi_line()
|
.multi_line(true)
|
||||||
.rows(10)
|
.rows(10)
|
||||||
.placeholder("Enter text here...")
|
.placeholder("Enter text here...")
|
||||||
.searchable(true)
|
.searchable(true)
|
||||||
|
|
@ -77,7 +77,7 @@ impl TextareaStory {
|
||||||
|
|
||||||
let textarea_no_wrap = cx.new(|cx| {
|
let textarea_no_wrap = cx.new(|cx| {
|
||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
.multi_line()
|
.multi_line(true)
|
||||||
.rows(6)
|
.rows(6)
|
||||||
.soft_wrap(false)
|
.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")
|
.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")
|
||||||
|
|
|
||||||
|
|
@ -567,12 +567,12 @@ impl TextElement {
|
||||||
bg_segments: &[(Range<usize>, Hsla)],
|
bg_segments: &[(Range<usize>, Hsla)],
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
) -> Vec<LineLayout> {
|
) -> Vec<LineLayout> {
|
||||||
let is_multi_line = state.mode.is_multi_line();
|
let is_single_line = state.mode.is_single_line();
|
||||||
let text_wrapper = &state.text_wrapper;
|
let text_wrapper = &state.text_wrapper;
|
||||||
let visible_range = &last_layout.visible_range;
|
let visible_range = &last_layout.visible_range;
|
||||||
let visible_range_offset = &last_layout.visible_range_offset;
|
let visible_range_offset = &last_layout.visible_range_offset;
|
||||||
|
|
||||||
if !is_multi_line {
|
if is_single_line {
|
||||||
let shaped_line = window.text_system().shape_line(
|
let shaped_line = window.text_system().shape_line(
|
||||||
display_text.to_string().into(),
|
display_text.to_string().into(),
|
||||||
font_size,
|
font_size,
|
||||||
|
|
@ -657,6 +657,7 @@ impl TextElement {
|
||||||
) -> Option<Vec<(Range<usize>, HighlightStyle)>> {
|
) -> Option<Vec<(Range<usize>, HighlightStyle)>> {
|
||||||
let state = self.state.read(cx);
|
let state = self.state.read(cx);
|
||||||
let text = &state.text;
|
let text = &state.text;
|
||||||
|
let is_multi_line = state.mode.is_multi_line();
|
||||||
|
|
||||||
let (highlighter, diagnostics) = match &state.mode {
|
let (highlighter, diagnostics) = match &state.mode {
|
||||||
InputMode::CodeEditor {
|
InputMode::CodeEditor {
|
||||||
|
|
@ -676,8 +677,13 @@ impl TextElement {
|
||||||
.skip(visible_range.start)
|
.skip(visible_range.start)
|
||||||
.take(visible_range.len())
|
.take(visible_range.len())
|
||||||
{
|
{
|
||||||
// +1 for `\n`
|
let line_len = if is_multi_line {
|
||||||
let line_len = line.len() + 1;
|
// +1 for `\n`
|
||||||
|
line.len() + 1
|
||||||
|
} else {
|
||||||
|
line.len()
|
||||||
|
};
|
||||||
|
|
||||||
let range = offset..offset + line_len;
|
let range = offset..offset + line_len;
|
||||||
let line_styles = highlighter.styles(&range, &cx.theme().highlight_theme);
|
let line_styles = highlighter.styles(&range, &cx.theme().highlight_theme);
|
||||||
styles = gpui::combine_highlights(styles, line_styles).collect();
|
styles = gpui::combine_highlights(styles, line_styles).collect();
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
use gpui::{
|
use gpui::{
|
||||||
point, px, Bounds, Context, EntityInputHandler as _, Hsla, Path, PathBuilder, Pixels,
|
Bounds, Context, EntityInputHandler as _, Hsla, Path, PathBuilder, Pixels, SharedString,
|
||||||
SharedString, TextRun, TextStyle, Window,
|
TextRun, TextStyle, Window, point, px,
|
||||||
};
|
};
|
||||||
use ropey::RopeSlice;
|
use ropey::RopeSlice;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
input::{
|
|
||||||
element::TextElement, mode::InputMode, Indent, IndentInline, InputState, LastLayout,
|
|
||||||
Outdent, OutdentInline,
|
|
||||||
},
|
|
||||||
RopeExt,
|
RopeExt,
|
||||||
|
input::{
|
||||||
|
Indent, IndentInline, InputState, LastLayout, Outdent, OutdentInline, element::TextElement,
|
||||||
|
mode::InputMode,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
|
@ -56,16 +56,22 @@ impl TabSize {
|
||||||
impl InputMode {
|
impl InputMode {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(super) fn is_indentable(&self) -> bool {
|
pub(super) fn is_indentable(&self) -> bool {
|
||||||
matches!(
|
match self {
|
||||||
self,
|
InputMode::PlainText { multi_line, .. } | InputMode::CodeEditor { multi_line, .. } => {
|
||||||
InputMode::MultiLine { .. } | InputMode::CodeEditor { .. }
|
*multi_line
|
||||||
)
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(super) fn has_indent_guides(&self) -> bool {
|
pub(super) fn has_indent_guides(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
InputMode::CodeEditor { indent_guides, .. } => *indent_guides,
|
InputMode::CodeEditor {
|
||||||
|
indent_guides,
|
||||||
|
multi_line,
|
||||||
|
..
|
||||||
|
} => *indent_guides && *multi_line,
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -73,7 +79,7 @@ impl InputMode {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(super) fn tab_size(&self) -> TabSize {
|
pub(super) fn tab_size(&self) -> TabSize {
|
||||||
match self {
|
match self {
|
||||||
InputMode::MultiLine { tab, .. } => *tab,
|
InputMode::PlainText { tab, .. } => *tab,
|
||||||
InputMode::CodeEditor { tab, .. } => *tab,
|
InputMode::CodeEditor { tab, .. } => *tab,
|
||||||
_ => TabSize::default(),
|
_ => TabSize::default(),
|
||||||
}
|
}
|
||||||
|
|
@ -168,7 +174,7 @@ impl InputState {
|
||||||
///
|
///
|
||||||
/// Only for [`InputMode::CodeEditor`] mode.
|
/// Only for [`InputMode::CodeEditor`] mode.
|
||||||
pub fn indent_guides(mut self, indent_guides: bool) -> Self {
|
pub fn indent_guides(mut self, indent_guides: bool) -> Self {
|
||||||
debug_assert!(self.mode.is_code_editor());
|
debug_assert!(self.mode.is_code_editor() && self.mode.is_multi_line());
|
||||||
if let InputMode::CodeEditor {
|
if let InputMode::CodeEditor {
|
||||||
indent_guides: l, ..
|
indent_guides: l, ..
|
||||||
} = &mut self.mode
|
} = &mut self.mode
|
||||||
|
|
@ -199,11 +205,11 @@ impl InputState {
|
||||||
|
|
||||||
/// Set the tab size for the input.
|
/// Set the tab size for the input.
|
||||||
///
|
///
|
||||||
/// Only for [`InputMode::MultiLine`] and [`InputMode::CodeEditor`] mode.
|
/// Only for [`InputMode::PlainText`] and [`InputMode::CodeEditor`] mode with multi_line.
|
||||||
pub fn tab_size(mut self, tab: TabSize) -> Self {
|
pub fn tab_size(mut self, tab: TabSize) -> Self {
|
||||||
debug_assert!(self.mode.is_multi_line() || self.mode.is_code_editor());
|
debug_assert!(self.mode.is_multi_line() || self.mode.is_code_editor());
|
||||||
match &mut self.mode {
|
match &mut self.mode {
|
||||||
InputMode::MultiLine { tab: t, .. } => *t = tab,
|
InputMode::PlainText { tab: t, .. } => *t = tab,
|
||||||
InputMode::CodeEditor { tab: t, .. } => *t = tab,
|
InputMode::CodeEditor { tab: t, .. } => *t = tab,
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,20 +10,23 @@ use crate::highlighter::DiagnosticSet;
|
||||||
use crate::highlighter::SyntaxHighlighter;
|
use crate::highlighter::SyntaxHighlighter;
|
||||||
use crate::input::{RopeExt as _, TabSize};
|
use crate::input::{RopeExt as _, TabSize};
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
#[derive(Clone)]
|
||||||
pub enum InputMode {
|
pub(crate) enum InputMode {
|
||||||
#[default]
|
/// A plain text input mode.
|
||||||
SingleLine,
|
PlainText {
|
||||||
MultiLine {
|
multi_line: bool,
|
||||||
tab: TabSize,
|
tab: TabSize,
|
||||||
rows: usize,
|
rows: usize,
|
||||||
},
|
},
|
||||||
|
/// An auto grow input mode.
|
||||||
AutoGrow {
|
AutoGrow {
|
||||||
rows: usize,
|
rows: usize,
|
||||||
min_rows: usize,
|
min_rows: usize,
|
||||||
max_rows: usize,
|
max_rows: usize,
|
||||||
},
|
},
|
||||||
|
/// A code editor input mode.
|
||||||
CodeEditor {
|
CodeEditor {
|
||||||
|
multi_line: bool,
|
||||||
tab: TabSize,
|
tab: TabSize,
|
||||||
rows: usize,
|
rows: usize,
|
||||||
/// Show line number
|
/// Show line number
|
||||||
|
|
@ -35,11 +38,58 @@ pub enum InputMode {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for InputMode {
|
||||||
|
fn default() -> Self {
|
||||||
|
InputMode::plain_text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
impl InputMode {
|
impl InputMode {
|
||||||
|
/// Create a plain input mode with default settings.
|
||||||
|
pub(super) fn plain_text() -> Self {
|
||||||
|
InputMode::PlainText {
|
||||||
|
multi_line: false,
|
||||||
|
tab: TabSize::default(),
|
||||||
|
rows: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a code editor input mode with default settings.
|
||||||
|
pub(super) fn code_editor(language: impl Into<SharedString>) -> Self {
|
||||||
|
InputMode::CodeEditor {
|
||||||
|
rows: 2,
|
||||||
|
multi_line: true,
|
||||||
|
tab: TabSize::default(),
|
||||||
|
language: language.into(),
|
||||||
|
highlighter: Rc::new(RefCell::new(None)),
|
||||||
|
line_number: true,
|
||||||
|
indent_guides: true,
|
||||||
|
diagnostics: DiagnosticSet::new(&Rope::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an auto grow input mode with given min and max rows.
|
||||||
|
pub(super) fn auto_grow(min_rows: usize, max_rows: usize) -> Self {
|
||||||
|
InputMode::AutoGrow {
|
||||||
|
rows: min_rows,
|
||||||
|
min_rows,
|
||||||
|
max_rows,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn multi_line(mut self, multi_line: bool) -> Self {
|
||||||
|
match &mut self {
|
||||||
|
InputMode::PlainText { multi_line: ml, .. } => *ml = multi_line,
|
||||||
|
InputMode::CodeEditor { multi_line: ml, .. } => *ml = multi_line,
|
||||||
|
InputMode::AutoGrow { .. } => {}
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(super) fn is_single_line(&self) -> bool {
|
pub(super) fn is_single_line(&self) -> bool {
|
||||||
matches!(self, InputMode::SingleLine)
|
!self.is_multi_line()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|
@ -54,15 +104,16 @@ impl InputMode {
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(super) fn is_multi_line(&self) -> bool {
|
pub(super) fn is_multi_line(&self) -> bool {
|
||||||
matches!(
|
match self {
|
||||||
self,
|
InputMode::PlainText { multi_line, .. } => *multi_line,
|
||||||
InputMode::MultiLine { .. } | InputMode::AutoGrow { .. } | InputMode::CodeEditor { .. }
|
InputMode::CodeEditor { multi_line, .. } => *multi_line,
|
||||||
)
|
InputMode::AutoGrow { max_rows, .. } => *max_rows > 1,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn set_rows(&mut self, new_rows: usize) {
|
pub(super) fn set_rows(&mut self, new_rows: usize) {
|
||||||
match self {
|
match self {
|
||||||
InputMode::MultiLine { rows, .. } => {
|
InputMode::PlainText { rows, .. } => {
|
||||||
*rows = new_rows;
|
*rows = new_rows;
|
||||||
}
|
}
|
||||||
InputMode::CodeEditor { rows, .. } => {
|
InputMode::CodeEditor { rows, .. } => {
|
||||||
|
|
@ -75,7 +126,6 @@ impl InputMode {
|
||||||
} => {
|
} => {
|
||||||
*rows = new_rows.clamp(*min_rows, *max_rows);
|
*rows = new_rows.clamp(*min_rows, *max_rows);
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,11 +140,14 @@ impl InputMode {
|
||||||
|
|
||||||
/// At least 1 row be return.
|
/// At least 1 row be return.
|
||||||
pub(super) fn rows(&self) -> usize {
|
pub(super) fn rows(&self) -> usize {
|
||||||
|
if !self.is_multi_line() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
InputMode::MultiLine { rows, .. } => *rows,
|
InputMode::PlainText { rows, .. } => *rows,
|
||||||
InputMode::CodeEditor { rows, .. } => *rows,
|
InputMode::CodeEditor { rows, .. } => *rows,
|
||||||
InputMode::AutoGrow { rows, .. } => *rows,
|
InputMode::AutoGrow { rows, .. } => *rows,
|
||||||
_ => 1,
|
|
||||||
}
|
}
|
||||||
.max(1)
|
.max(1)
|
||||||
}
|
}
|
||||||
|
|
@ -103,7 +156,6 @@ impl InputMode {
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub(super) fn min_rows(&self) -> usize {
|
pub(super) fn min_rows(&self) -> usize {
|
||||||
match self {
|
match self {
|
||||||
InputMode::MultiLine { .. } | InputMode::CodeEditor { .. } => 1,
|
|
||||||
InputMode::AutoGrow { min_rows, .. } => *min_rows,
|
InputMode::AutoGrow { min_rows, .. } => *min_rows,
|
||||||
_ => 1,
|
_ => 1,
|
||||||
}
|
}
|
||||||
|
|
@ -112,10 +164,13 @@ impl InputMode {
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub(super) fn max_rows(&self) -> usize {
|
pub(super) fn max_rows(&self) -> usize {
|
||||||
|
if !self.is_multi_line() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
InputMode::MultiLine { .. } | InputMode::CodeEditor { .. } => usize::MAX,
|
|
||||||
InputMode::AutoGrow { max_rows, .. } => *max_rows,
|
InputMode::AutoGrow { max_rows, .. } => *max_rows,
|
||||||
_ => 1,
|
_ => usize::MAX,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,7 +179,11 @@ impl InputMode {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(super) fn line_number(&self) -> bool {
|
pub(super) fn line_number(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
InputMode::CodeEditor { line_number, .. } => *line_number,
|
InputMode::CodeEditor {
|
||||||
|
line_number,
|
||||||
|
multi_line,
|
||||||
|
..
|
||||||
|
} => *line_number && *multi_line,
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -202,3 +261,88 @@ impl InputMode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use ropey::Rope;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
highlighter::DiagnosticSet,
|
||||||
|
input::{TabSize, mode::InputMode},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_code_editor() {
|
||||||
|
let mode = InputMode::code_editor("rust");
|
||||||
|
assert_eq!(mode.is_code_editor(), true);
|
||||||
|
assert_eq!(mode.is_multi_line(), true);
|
||||||
|
assert_eq!(mode.is_single_line(), false);
|
||||||
|
assert_eq!(mode.line_number(), true);
|
||||||
|
assert_eq!(mode.has_indent_guides(), true);
|
||||||
|
assert_eq!(mode.max_rows(), usize::MAX);
|
||||||
|
assert_eq!(mode.min_rows(), 1);
|
||||||
|
|
||||||
|
let mode = InputMode::CodeEditor {
|
||||||
|
multi_line: false,
|
||||||
|
line_number: true,
|
||||||
|
indent_guides: true,
|
||||||
|
rows: 0,
|
||||||
|
tab: Default::default(),
|
||||||
|
language: "rust".into(),
|
||||||
|
highlighter: Default::default(),
|
||||||
|
diagnostics: DiagnosticSet::new(&Rope::new()),
|
||||||
|
};
|
||||||
|
assert_eq!(mode.is_code_editor(), true);
|
||||||
|
assert_eq!(mode.is_multi_line(), false);
|
||||||
|
assert_eq!(mode.is_single_line(), true);
|
||||||
|
assert_eq!(mode.line_number(), false);
|
||||||
|
assert_eq!(mode.has_indent_guides(), false);
|
||||||
|
assert_eq!(mode.max_rows(), 1);
|
||||||
|
assert_eq!(mode.min_rows(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_plain() {
|
||||||
|
let mode = InputMode::PlainText {
|
||||||
|
multi_line: true,
|
||||||
|
tab: TabSize::default(),
|
||||||
|
rows: 5,
|
||||||
|
};
|
||||||
|
assert_eq!(mode.is_code_editor(), false);
|
||||||
|
assert_eq!(mode.is_multi_line(), true);
|
||||||
|
assert_eq!(mode.is_single_line(), false);
|
||||||
|
assert_eq!(mode.line_number(), false);
|
||||||
|
assert_eq!(mode.rows(), 5);
|
||||||
|
assert_eq!(mode.max_rows(), usize::MAX);
|
||||||
|
assert_eq!(mode.min_rows(), 1);
|
||||||
|
|
||||||
|
let mode = InputMode::plain_text();
|
||||||
|
assert_eq!(mode.is_code_editor(), false);
|
||||||
|
assert_eq!(mode.is_multi_line(), false);
|
||||||
|
assert_eq!(mode.is_single_line(), true);
|
||||||
|
assert_eq!(mode.line_number(), false);
|
||||||
|
assert_eq!(mode.max_rows(), 1);
|
||||||
|
assert_eq!(mode.min_rows(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_auto_grow() {
|
||||||
|
let mut mode = InputMode::auto_grow(2, 5);
|
||||||
|
assert_eq!(mode.is_code_editor(), false);
|
||||||
|
assert_eq!(mode.is_multi_line(), true);
|
||||||
|
assert_eq!(mode.is_single_line(), false);
|
||||||
|
assert_eq!(mode.line_number(), false);
|
||||||
|
assert_eq!(mode.rows(), 2);
|
||||||
|
assert_eq!(mode.max_rows(), 5);
|
||||||
|
assert_eq!(mode.min_rows(), 2);
|
||||||
|
|
||||||
|
mode.set_rows(4);
|
||||||
|
assert_eq!(mode.rows(), 4);
|
||||||
|
|
||||||
|
mode.set_rows(1);
|
||||||
|
assert_eq!(mode.rows(), 2);
|
||||||
|
|
||||||
|
mode.set_rows(10);
|
||||||
|
assert_eq!(mode.rows(), 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,15 +12,14 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use ropey::{Rope, RopeSlice};
|
use ropey::{Rope, RopeSlice};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use sum_tree::Bias;
|
use sum_tree::Bias;
|
||||||
use unicode_segmentation::*;
|
use unicode_segmentation::*;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
TabSize, blink_cursor::BlinkCursor, change::Change, element::TextElement,
|
blink_cursor::BlinkCursor, change::Change, element::TextElement, mask_pattern::MaskPattern,
|
||||||
mask_pattern::MaskPattern, mode::InputMode, number_input, text_wrapper::TextWrapper,
|
mode::InputMode, number_input, text_wrapper::TextWrapper,
|
||||||
};
|
};
|
||||||
use crate::Size;
|
use crate::Size;
|
||||||
use crate::actions::{SelectDown, SelectLeft, SelectRight, SelectUp};
|
use crate::actions::{SelectDown, SelectLeft, SelectRight, SelectUp};
|
||||||
|
|
@ -391,7 +390,7 @@ impl InputState {
|
||||||
loading: false,
|
loading: false,
|
||||||
pattern: None,
|
pattern: None,
|
||||||
validate: None,
|
validate: None,
|
||||||
mode: InputMode::SingleLine,
|
mode: InputMode::default(),
|
||||||
last_layout: None,
|
last_layout: None,
|
||||||
last_bounds: None,
|
last_bounds: None,
|
||||||
last_selected_range: None,
|
last_selected_range: None,
|
||||||
|
|
@ -417,24 +416,17 @@ impl InputState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set Input to use [`InputMode::MultiLine`] mode.
|
/// Set Input to use multi line mode.
|
||||||
///
|
///
|
||||||
/// Default rows is 2.
|
/// Default rows is 2.
|
||||||
pub fn multi_line(mut self) -> Self {
|
pub fn multi_line(mut self, multi_line: bool) -> Self {
|
||||||
self.mode = InputMode::MultiLine {
|
self.mode = self.mode.multi_line(multi_line);
|
||||||
rows: 2,
|
|
||||||
tab: TabSize::default(),
|
|
||||||
};
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set Input to use [`InputMode::AutoGrow`] mode with min, max rows limit.
|
/// Set Input to use [`InputMode::AutoGrow`] mode with min, max rows limit.
|
||||||
pub fn auto_grow(mut self, min_rows: usize, max_rows: usize) -> Self {
|
pub fn auto_grow(mut self, min_rows: usize, max_rows: usize) -> Self {
|
||||||
self.mode = InputMode::AutoGrow {
|
self.mode = InputMode::auto_grow(min_rows, max_rows);
|
||||||
rows: min_rows,
|
|
||||||
min_rows: min_rows,
|
|
||||||
max_rows: max_rows,
|
|
||||||
};
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -445,7 +437,9 @@ impl InputState {
|
||||||
/// - line_number: true
|
/// - line_number: true
|
||||||
/// - tab_size: 2
|
/// - tab_size: 2
|
||||||
/// - hard_tabs: false
|
/// - hard_tabs: false
|
||||||
/// - height: full
|
/// - height: 100%
|
||||||
|
/// - multi_line: true
|
||||||
|
/// - indent_guides: true
|
||||||
///
|
///
|
||||||
/// If `highlighter` is None, will use the default highlighter.
|
/// If `highlighter` is None, will use the default highlighter.
|
||||||
///
|
///
|
||||||
|
|
@ -459,15 +453,7 @@ impl InputState {
|
||||||
/// - Large Text support, up to 50K lines.
|
/// - Large Text support, up to 50K lines.
|
||||||
pub fn code_editor(mut self, language: impl Into<SharedString>) -> Self {
|
pub fn code_editor(mut self, language: impl Into<SharedString>) -> Self {
|
||||||
let language: SharedString = language.into();
|
let language: SharedString = language.into();
|
||||||
self.mode = InputMode::CodeEditor {
|
self.mode = InputMode::code_editor(language);
|
||||||
rows: 2,
|
|
||||||
tab: TabSize::default(),
|
|
||||||
language,
|
|
||||||
highlighter: Rc::new(RefCell::new(None)),
|
|
||||||
line_number: true,
|
|
||||||
indent_guides: true,
|
|
||||||
diagnostics: DiagnosticSet::new(&Rope::new()),
|
|
||||||
};
|
|
||||||
self.searchable = true;
|
self.searchable = true;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
@ -487,7 +473,7 @@ impl InputState {
|
||||||
|
|
||||||
/// Set enable/disable line number, only for [`InputMode::CodeEditor`] mode.
|
/// Set enable/disable line number, only for [`InputMode::CodeEditor`] mode.
|
||||||
pub fn line_number(mut self, line_number: bool) -> Self {
|
pub fn line_number(mut self, line_number: bool) -> Self {
|
||||||
debug_assert!(self.mode.is_code_editor());
|
debug_assert!(self.mode.is_code_editor() && self.mode.is_multi_line());
|
||||||
if let InputMode::CodeEditor { line_number: l, .. } = &mut self.mode {
|
if let InputMode::CodeEditor { line_number: l, .. } = &mut self.mode {
|
||||||
*l = line_number;
|
*l = line_number;
|
||||||
}
|
}
|
||||||
|
|
@ -496,7 +482,7 @@ impl InputState {
|
||||||
|
|
||||||
/// Set line number, only for [`InputMode::CodeEditor`] mode.
|
/// Set line number, only for [`InputMode::CodeEditor`] mode.
|
||||||
pub fn set_line_number(&mut self, line_number: bool, _: &mut Window, cx: &mut Context<Self>) {
|
pub fn set_line_number(&mut self, line_number: bool, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
debug_assert!(self.mode.is_code_editor());
|
debug_assert!(self.mode.is_code_editor() && self.mode.is_multi_line());
|
||||||
if let InputMode::CodeEditor { line_number: l, .. } = &mut self.mode {
|
if let InputMode::CodeEditor { line_number: l, .. } = &mut self.mode {
|
||||||
*l = line_number;
|
*l = line_number;
|
||||||
}
|
}
|
||||||
|
|
@ -510,7 +496,9 @@ impl InputState {
|
||||||
/// default: 2
|
/// default: 2
|
||||||
pub fn rows(mut self, rows: usize) -> Self {
|
pub fn rows(mut self, rows: usize) -> Self {
|
||||||
match &mut self.mode {
|
match &mut self.mode {
|
||||||
InputMode::MultiLine { rows: r, .. } => *r = rows,
|
InputMode::PlainText { rows: r, .. } | InputMode::CodeEditor { rows: r, .. } => {
|
||||||
|
*r = rows
|
||||||
|
}
|
||||||
InputMode::AutoGrow {
|
InputMode::AutoGrow {
|
||||||
max_rows: max_r,
|
max_rows: max_r,
|
||||||
rows: r,
|
rows: r,
|
||||||
|
|
@ -519,7 +507,6 @@ impl InputState {
|
||||||
*r = rows;
|
*r = rows;
|
||||||
*max_r = rows;
|
*max_r = rows;
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,61 +15,53 @@ use gpui_component::input::{InputState, Input};
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Basic Textarea
|
### Textarea
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
let textarea = cx.new(|cx|
|
let state = cx.new(|cx|
|
||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
.multi_line()
|
.multi_line(true)
|
||||||
.placeholder("Enter your message...")
|
.placeholder("Enter your message...")
|
||||||
);
|
);
|
||||||
|
|
||||||
Input::new(&textarea)
|
Input::new(&state)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Fixed Height Textarea
|
With fixed height Textarea:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
let textarea = cx.new(|cx|
|
let state = cx.new(|cx|
|
||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
.multi_line()
|
.multi_line(true)
|
||||||
.rows(10) // Set number of rows
|
.rows(10) // Set number of rows
|
||||||
.placeholder("Enter text here...")
|
.placeholder("Enter text here...")
|
||||||
);
|
);
|
||||||
|
|
||||||
Input::new(&textarea)
|
Input::new(&state)
|
||||||
.h(px(320.)) // Set explicit height
|
.h(px(320.)) // Set explicit height
|
||||||
```
|
```
|
||||||
|
|
||||||
### Auto-Resizing Textarea
|
### AutoGrow
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
let textarea = cx.new(|cx|
|
let state = cx.new(|cx|
|
||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
.auto_grow(1, 5) // min_rows: 1, max_rows: 5
|
.auto_grow(1, 5) // min_rows: 1, max_rows: 5
|
||||||
.placeholder("Type here and watch it grow...")
|
.placeholder("Type here and watch it grow...")
|
||||||
);
|
);
|
||||||
|
|
||||||
Input::new(&textarea)
|
Input::new(&state)
|
||||||
```
|
```
|
||||||
|
|
||||||
### With Default Content
|
### CodeEditor
|
||||||
|
|
||||||
|
GPUI Component's `InputState` supports a code editor mode with syntax highlighting, line numbers, and search functionality.
|
||||||
|
|
||||||
|
It design for high performance and can handle large files efficiently. We
|
||||||
|
used [tree-sitter](https://tree-sitter.github.io/tree-sitter/) for syntax highlighting, and [ropey](https://github.com/cessen/ropey) for text storage and manipulation.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
let textarea = cx.new(|cx|
|
let state = cx.new(|cx|
|
||||||
InputState::new(window, cx)
|
|
||||||
.multi_line()
|
|
||||||
.rows(6)
|
|
||||||
.default_value("Hello World!\n\nThis is a multi-line textarea with default content.")
|
|
||||||
);
|
|
||||||
|
|
||||||
Input::new(&textarea)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Code Editor Mode
|
|
||||||
|
|
||||||
```rust
|
|
||||||
let code_editor = cx.new(|cx|
|
|
||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
.code_editor("rust") // Language for syntax highlighting
|
.code_editor("rust") // Language for syntax highlighting
|
||||||
.line_number(true) // Show line numbers
|
.line_number(true) // Show line numbers
|
||||||
|
|
@ -77,142 +69,129 @@ let code_editor = cx.new(|cx|
|
||||||
.default_value("fn main() {\n println!(\"Hello, world!\");\n}")
|
.default_value("fn main() {\n println!(\"Hello, world!\");\n}")
|
||||||
);
|
);
|
||||||
|
|
||||||
Input::new(&code_editor)
|
Input::new(&state)
|
||||||
.h_full() // Full height
|
.h_full() // Full height
|
||||||
```
|
```
|
||||||
|
|
||||||
### Textarea with Custom Tab Size
|
#### Single Line Mode
|
||||||
|
|
||||||
|
Sometimes you may want to use the code editor features but restrict input to a single line, for example for code snippets or commands.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let state = cx.new(|cx|
|
||||||
|
InputState::new(window, cx)
|
||||||
|
.code_editor("rust")
|
||||||
|
.multi_line(false) // Single line
|
||||||
|
.default_value("println!(\"Hello, world!\");")
|
||||||
|
);
|
||||||
|
|
||||||
|
Input::new(&state)
|
||||||
|
```
|
||||||
|
|
||||||
|
### TabSize
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use gpui_component::input::TabSize;
|
use gpui_component::input::TabSize;
|
||||||
|
|
||||||
let textarea = cx.new(|cx|
|
let state = cx.new(|cx|
|
||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
.multi_line()
|
.multi_line(true)
|
||||||
.tab_size(TabSize {
|
.tab_size(TabSize {
|
||||||
tab_size: 4,
|
tab_size: 4,
|
||||||
hard_tabs: false, // Use spaces instead of tabs
|
hard_tabs: false, // Use spaces instead of tabs
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
Input::new(&textarea)
|
Input::new(&state)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Searchable Textarea
|
### Searchable
|
||||||
|
|
||||||
|
The search feature allows for all multi-line inputs to support searching through the content using `Ctrl+F` (or `Cmd+F` on Mac).
|
||||||
|
|
||||||
|
It provides a search bar with options to navigate between matches and highlight them.
|
||||||
|
|
||||||
|
Use `searchable` method to enable:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
let textarea = cx.new(|cx|
|
let state = cx.new(|cx|
|
||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
.multi_line()
|
.multi_line(true)
|
||||||
.searchable(true) // Enable Ctrl+F search
|
.searchable(true) // Enable Ctrl+F search
|
||||||
.rows(15)
|
.rows(15)
|
||||||
.default_value("Search through this content...")
|
.default_value("Search through this content...")
|
||||||
);
|
);
|
||||||
|
|
||||||
Input::new(&textarea)
|
Input::new(&state)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Soft Wrap Control
|
### SoftWrap
|
||||||
|
|
||||||
|
By default multi-line inputs have soft wrapping enabled, meaning long lines will wrap to fit the width of the textarea.
|
||||||
|
|
||||||
|
You can disable soft wrapping to allow horizontal scrolling instead:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// With soft wrap (default)
|
// With soft wrap (default)
|
||||||
let textarea_wrap = cx.new(|cx|
|
let state = cx.new(|cx|
|
||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
.multi_line()
|
.multi_line(true)
|
||||||
.soft_wrap(true)
|
.soft_wrap(true)
|
||||||
.rows(6)
|
.rows(6)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Without soft wrap (horizontal scrolling)
|
// Without soft wrap (horizontal scrolling)
|
||||||
let textarea_no_wrap = cx.new(|cx|
|
let state = cx.new(|cx|
|
||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
.multi_line()
|
.multi_line(true)
|
||||||
.soft_wrap(false)
|
.soft_wrap(false)
|
||||||
.rows(6)
|
.rows(6)
|
||||||
.default_value("This is a very long line that will not wrap automatically but will show horizontal scrollbar instead.")
|
.default_value("This is a very long line that will not wrap automatically but will show horizontal scrollbar instead.")
|
||||||
);
|
);
|
||||||
|
|
||||||
v_flex()
|
|
||||||
.gap_4()
|
|
||||||
.child(Input::new(&textarea_wrap))
|
|
||||||
.child(Input::new(&textarea_no_wrap))
|
|
||||||
```
|
|
||||||
|
|
||||||
### Character Counting
|
|
||||||
|
|
||||||
```rust
|
|
||||||
struct TextareaView {
|
|
||||||
textarea: Entity<InputState>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for TextareaView {
|
|
||||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
let char_count = self.textarea.read(cx).value().len();
|
|
||||||
let max_chars = 500;
|
|
||||||
|
|
||||||
v_flex()
|
|
||||||
.gap_2()
|
|
||||||
.child(
|
|
||||||
Input::new(&self.textarea)
|
|
||||||
.h(px(120.))
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_right()
|
|
||||||
.text_sm()
|
|
||||||
.text_color(if char_count > max_chars {
|
|
||||||
cx.theme().destructive
|
|
||||||
} else {
|
|
||||||
cx.theme().muted_foreground
|
|
||||||
})
|
|
||||||
.child(format!("{}/{}", char_count, max_chars))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Text Manipulation
|
### Text Manipulation
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// Insert text at cursor position
|
// Insert text at cursor position
|
||||||
textarea.update(cx, |input, cx| {
|
state.update(cx, |state, cx| {
|
||||||
input.insert("inserted text", window, cx);
|
state.insert("inserted text", window, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Replace all content
|
// Replace all content
|
||||||
textarea.update(cx, |input, cx| {
|
state.update(cx, |state, cx| {
|
||||||
input.replace("new content", window, cx);
|
state.replace("new content", window, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set cursor position
|
// Set cursor position
|
||||||
textarea.update(cx, |input, cx| {
|
state.update(cx, |state, cx| {
|
||||||
input.set_cursor_position(Position { line: 2, character: 5 }, window, cx);
|
state.set_cursor_position(Position { line: 2, character: 5 }, window, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get cursor position
|
// Get cursor position
|
||||||
let position = textarea.read(cx).cursor_position();
|
let position = state.read(cx).cursor_position();
|
||||||
println!("Line: {}, Column: {}", position.line, position.character);
|
println!("Line: {}, Column: {}", position.line, position.character);
|
||||||
```
|
```
|
||||||
|
|
||||||
### Validation
|
### Validation
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
let textarea = cx.new(|cx|
|
let state = cx.new(|cx|
|
||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
.multi_line()
|
.multi_line(true)
|
||||||
.validate(|text, _| {
|
.validate(|text, _| {
|
||||||
// Validate that content is not empty and under 1000 chars
|
// Validate that content is not empty and under 1000 chars
|
||||||
!text.trim().is_empty() && text.len() <= 1000
|
!text.trim().is_empty() && text.len() <= 1000
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
Input::new(&textarea)
|
Input::new(&state)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Handle Events
|
### Handle Events
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
cx.subscribe_in(&textarea, window, |view, state, event, window, cx| {
|
cx.subscribe_in(&state, window, |view, state, event, window, cx| {
|
||||||
match event {
|
match event {
|
||||||
InputEvent::Change => {
|
InputEvent::Change => {
|
||||||
let content = state.read(cx).value();
|
let content = state.read(cx).value();
|
||||||
|
|
@ -234,7 +213,7 @@ cx.subscribe_in(&textarea, window, |view, state, event, window, cx| {
|
||||||
### Disabled State
|
### Disabled State
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
Input::new(&textarea)
|
Input::new(&state)
|
||||||
.disabled(true)
|
.disabled(true)
|
||||||
.h(px(200.))
|
.h(px(200.))
|
||||||
```
|
```
|
||||||
|
|
@ -243,7 +222,7 @@ Input::new(&textarea)
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// Without default appearance
|
// Without default appearance
|
||||||
Input::new(&textarea)
|
Input::new(&state)
|
||||||
.appearance(false)
|
.appearance(false)
|
||||||
.h(px(200.))
|
.h(px(200.))
|
||||||
|
|
||||||
|
|
@ -255,80 +234,25 @@ div()
|
||||||
.rounded_lg()
|
.rounded_lg()
|
||||||
.p_4()
|
.p_4()
|
||||||
.child(
|
.child(
|
||||||
Input::new(&textarea)
|
Input::new(&state)
|
||||||
.appearance(false)
|
.appearance(false)
|
||||||
.h(px(150.))
|
.h(px(150.))
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
## API Reference
|
|
||||||
|
|
||||||
### InputState (Multi-line Methods)
|
|
||||||
|
|
||||||
| Method | Description |
|
|
||||||
| -------------------------------------- | ------------------------------------------------ |
|
|
||||||
| `multi_line()` | Enable multi-line mode with 2 rows default |
|
|
||||||
| `auto_grow(min, max)` | Enable auto-resize between min and max rows |
|
|
||||||
| `code_editor(language)` | Enable code editor mode with syntax highlighting |
|
|
||||||
| `rows(count)` | Set number of visible rows |
|
|
||||||
| `tab_size(TabSize)` | Configure tab behavior |
|
|
||||||
| `searchable(bool)` | Enable/disable search (Ctrl+F) |
|
|
||||||
| `soft_wrap(bool)` | Enable/disable text wrapping |
|
|
||||||
| `line_number(bool)` | Show/hide line numbers (code editor only) |
|
|
||||||
| `cursor_position()` | Get current cursor position as `Position` |
|
|
||||||
| `set_cursor_position(pos, window, cx)` | Set cursor to specific line/column |
|
|
||||||
| `insert(text, window, cx)` | Insert text at cursor |
|
|
||||||
| `replace(text, window, cx)` | Replace all content |
|
|
||||||
|
|
||||||
### Input (Multi-line Methods)
|
|
||||||
|
|
||||||
| Method | Description |
|
|
||||||
| ----------- | -------------------------- |
|
|
||||||
| `h(height)` | Set explicit height |
|
|
||||||
| `h_full()` | Take full available height |
|
|
||||||
|
|
||||||
### Position
|
|
||||||
|
|
||||||
| Field | Description |
|
|
||||||
| ----------- | ---------------------------------- |
|
|
||||||
| `line` | 0-based line number |
|
|
||||||
| `character` | 0-based character position in line |
|
|
||||||
|
|
||||||
### TabSize
|
|
||||||
|
|
||||||
| Field | Description |
|
|
||||||
| ----------- | ------------------------------------------ |
|
|
||||||
| `tab_size` | Number of spaces per tab (default: 2) |
|
|
||||||
| `hard_tabs` | Use actual tab characters (default: false) |
|
|
||||||
|
|
||||||
### Keyboard Shortcuts
|
|
||||||
|
|
||||||
| Shortcut | Action |
|
|
||||||
| ------------- | --------------------------- |
|
|
||||||
| `Enter` | Insert new line |
|
|
||||||
| `Shift+Enter` | Insert new line (secondary) |
|
|
||||||
| `Tab` | Indent line/selection |
|
|
||||||
| `Shift+Tab` | Outdent line/selection |
|
|
||||||
| `Ctrl/Cmd+A` | Select all |
|
|
||||||
| `Ctrl/Cmd+Z` | Undo |
|
|
||||||
| `Ctrl/Cmd+Y` | Redo |
|
|
||||||
| `Ctrl/Cmd+F` | Open search (if enabled) |
|
|
||||||
| `Ctrl/Cmd+[` | Outdent block |
|
|
||||||
| `Ctrl/Cmd+]` | Indent block |
|
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
### Comment Box
|
### Comment Box
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
struct CommentBox {
|
struct CommentBox {
|
||||||
textarea: Entity<InputState>,
|
state: Entity<InputState>,
|
||||||
char_limit: usize,
|
char_limit: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommentBox {
|
impl CommentBox {
|
||||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let textarea = cx.new(|cx|
|
let state = cx.new(|cx|
|
||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
.auto_grow(3, 8)
|
.auto_grow(3, 8)
|
||||||
.placeholder("Write your comment...")
|
.placeholder("Write your comment...")
|
||||||
|
|
@ -336,7 +260,7 @@ impl CommentBox {
|
||||||
);
|
);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
textarea,
|
state,
|
||||||
char_limit: 500,
|
char_limit: 500,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -344,13 +268,13 @@ impl CommentBox {
|
||||||
|
|
||||||
impl Render for CommentBox {
|
impl Render for CommentBox {
|
||||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let content = self.textarea.read(cx).value();
|
let content = self.state.read(cx).value();
|
||||||
let char_count = content.len();
|
let char_count = content.len();
|
||||||
let remaining = self.char_limit.saturating_sub(char_count);
|
let remaining = self.char_limit.saturating_sub(char_count);
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(Input::new(&self.textarea))
|
.child(Input::new(&self.state))
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
|
|
@ -457,22 +381,3 @@ impl Render for TextEditor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Performance Notes
|
|
||||||
|
|
||||||
- Optimized for large text content (up to 200K lines in code editor mode)
|
|
||||||
- Efficient text wrapping and line measurement
|
|
||||||
- Virtual scrolling for long documents
|
|
||||||
- Minimal re-renders on text changes
|
|
||||||
- Efficient syntax highlighting with tree-sitter
|
|
||||||
- Smart auto-grow calculations
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
1. **Auto-resize**: Use `auto_grow()` for dynamic content like comments or messages
|
|
||||||
2. **Fixed size**: Use `multi_line().rows(n)` for consistent layouts like forms
|
|
||||||
3. **Code editing**: Use `code_editor()` for syntax-aware editing
|
|
||||||
4. **Validation**: Always validate long-form content on the client side
|
|
||||||
5. **Character limits**: Show character counters for user guidance
|
|
||||||
6. **Search**: Enable search for long content areas
|
|
||||||
7. **Soft wrap**: Disable for code, enable for prose
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue