chore: refactor the input component (#165)
* refactor the input component * fix clippy * clean up
This commit is contained in:
74
Cargo.lock
generated
74
Cargo.lock
generated
@@ -541,7 +541,7 @@ dependencies = [
|
|||||||
"bitflags 2.9.4",
|
"bitflags 2.9.4",
|
||||||
"cexpr",
|
"cexpr",
|
||||||
"clang-sys",
|
"clang-sys",
|
||||||
"itertools 0.13.0",
|
"itertools 0.11.0",
|
||||||
"log",
|
"log",
|
||||||
"prettyplease",
|
"prettyplease",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -561,7 +561,7 @@ dependencies = [
|
|||||||
"bitflags 2.9.4",
|
"bitflags 2.9.4",
|
||||||
"cexpr",
|
"cexpr",
|
||||||
"clang-sys",
|
"clang-sys",
|
||||||
"itertools 0.13.0",
|
"itertools 0.11.0",
|
||||||
"log",
|
"log",
|
||||||
"prettyplease",
|
"prettyplease",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -1128,7 +1128,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collections"
|
name = "collections"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#681a4adc42df0c93b95fd1a36a3ad47eaca086c0"
|
source = "git+https://github.com/zed-industries/zed#5612a961b02522a5e3f6292873fec1f853643734"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"rustc-hash 2.1.1",
|
"rustc-hash 2.1.1",
|
||||||
@@ -1569,7 +1569,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_refineable"
|
name = "derive_refineable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#681a4adc42df0c93b95fd1a36a3ad47eaca086c0"
|
source = "git+https://github.com/zed-industries/zed#5612a961b02522a5e3f6292873fec1f853643734"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -2015,6 +2015,15 @@ version = "1.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8"
|
checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fluent-uri"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flume"
|
name = "flume"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
@@ -2496,7 +2505,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui"
|
name = "gpui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#681a4adc42df0c93b95fd1a36a3ad47eaca086c0"
|
source = "git+https://github.com/zed-industries/zed#5612a961b02522a5e3f6292873fec1f853643734"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"as-raw-xcb-connection",
|
"as-raw-xcb-connection",
|
||||||
@@ -2590,7 +2599,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui_macros"
|
name = "gpui_macros"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#681a4adc42df0c93b95fd1a36a3ad47eaca086c0"
|
source = "git+https://github.com/zed-industries/zed#5612a961b02522a5e3f6292873fec1f853643734"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -2602,7 +2611,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui_tokio"
|
name = "gpui_tokio"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#681a4adc42df0c93b95fd1a36a3ad47eaca086c0"
|
source = "git+https://github.com/zed-industries/zed#5612a961b02522a5e3f6292873fec1f853643734"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"gpui",
|
"gpui",
|
||||||
@@ -2822,7 +2831,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "http_client"
|
name = "http_client"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#681a4adc42df0c93b95fd1a36a3ad47eaca086c0"
|
source = "git+https://github.com/zed-industries/zed#5612a961b02522a5e3f6292873fec1f853643734"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -2842,7 +2851,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "http_client_tls"
|
name = "http_client_tls"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#681a4adc42df0c93b95fd1a36a3ad47eaca086c0"
|
source = "git+https://github.com/zed-industries/zed#5612a961b02522a5e3f6292873fec1f853643734"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-platform-verifier",
|
"rustls-platform-verifier",
|
||||||
@@ -3544,6 +3553,19 @@ version = "0.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lsp-types"
|
||||||
|
version = "0.97.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "53353550a17c04ac46c585feb189c2db82154fc84b79c7a66c96c2c644f66071"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"fluent-uri",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_repr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lyon"
|
name = "lyon"
|
||||||
version = "1.0.16"
|
version = "1.0.16"
|
||||||
@@ -3634,7 +3656,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "media"
|
name = "media"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#681a4adc42df0c93b95fd1a36a3ad47eaca086c0"
|
source = "git+https://github.com/zed-industries/zed#5612a961b02522a5e3f6292873fec1f853643734"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bindgen 0.71.1",
|
"bindgen 0.71.1",
|
||||||
@@ -4477,7 +4499,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "perf"
|
name = "perf"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#681a4adc42df0c93b95fd1a36a3ad47eaca086c0"
|
source = "git+https://github.com/zed-industries/zed#5612a961b02522a5e3f6292873fec1f853643734"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"collections",
|
"collections",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -5085,7 +5107,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "refineable"
|
name = "refineable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#681a4adc42df0c93b95fd1a36a3ad47eaca086c0"
|
source = "git+https://github.com/zed-industries/zed#5612a961b02522a5e3f6292873fec1f853643734"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"derive_refineable",
|
"derive_refineable",
|
||||||
"workspace-hack",
|
"workspace-hack",
|
||||||
@@ -5239,7 +5261,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest_client"
|
name = "reqwest_client"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#681a4adc42df0c93b95fd1a36a3ad47eaca086c0"
|
source = "git+https://github.com/zed-industries/zed#5612a961b02522a5e3f6292873fec1f853643734"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -5291,6 +5313,21 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rope"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/zed-industries/zed#5612a961b02522a5e3f6292873fec1f853643734"
|
||||||
|
dependencies = [
|
||||||
|
"arrayvec",
|
||||||
|
"log",
|
||||||
|
"rayon",
|
||||||
|
"smallvec",
|
||||||
|
"sum_tree",
|
||||||
|
"unicode-segmentation",
|
||||||
|
"util",
|
||||||
|
"workspace-hack",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "roxmltree"
|
name = "roxmltree"
|
||||||
version = "0.20.0"
|
version = "0.20.0"
|
||||||
@@ -5774,7 +5811,7 @@ checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "semantic_version"
|
name = "semantic_version"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#681a4adc42df0c93b95fd1a36a3ad47eaca086c0"
|
source = "git+https://github.com/zed-industries/zed#5612a961b02522a5e3f6292873fec1f853643734"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -6226,7 +6263,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "sum_tree"
|
name = "sum_tree"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#681a4adc42df0c93b95fd1a36a3ad47eaca086c0"
|
source = "git+https://github.com/zed-industries/zed#5612a961b02522a5e3f6292873fec1f853643734"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"log",
|
"log",
|
||||||
@@ -7086,15 +7123,18 @@ dependencies = [
|
|||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
"linkify",
|
"linkify",
|
||||||
"log",
|
"log",
|
||||||
|
"lsp-types",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"regex",
|
"regex",
|
||||||
"registry",
|
"registry",
|
||||||
|
"rope",
|
||||||
"rust-i18n",
|
"rust-i18n",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"smol",
|
"smol",
|
||||||
|
"sum_tree",
|
||||||
"theme",
|
"theme",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"uuid",
|
"uuid",
|
||||||
@@ -7269,7 +7309,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "util"
|
name = "util"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#681a4adc42df0c93b95fd1a36a3ad47eaca086c0"
|
source = "git+https://github.com/zed-industries/zed#5612a961b02522a5e3f6292873fec1f853643734"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-fs",
|
"async-fs",
|
||||||
@@ -7304,7 +7344,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "util_macros"
|
name = "util_macros"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#681a4adc42df0c93b95fd1a36a3ad47eaca086c0"
|
source = "git+https://github.com/zed-industries/zed#5612a961b02522a5e3f6292873fec1f853643734"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"perf",
|
"perf",
|
||||||
"quote",
|
"quote",
|
||||||
|
|||||||
@@ -89,11 +89,8 @@ impl Chat {
|
|||||||
let input = cx.new(|cx| {
|
let input = cx.new(|cx| {
|
||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
.placeholder(t!("chat.placeholder"))
|
.placeholder(t!("chat.placeholder"))
|
||||||
.multi_line()
|
|
||||||
.prevent_new_line_on_enter()
|
|
||||||
.rows(1)
|
|
||||||
.multi_line()
|
|
||||||
.auto_grow(1, 20)
|
.auto_grow(1, 20)
|
||||||
|
.prevent_new_line_on_enter()
|
||||||
.clean_on_escape()
|
.clean_on_escape()
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -155,14 +152,8 @@ impl Chat {
|
|||||||
&input,
|
&input,
|
||||||
window,
|
window,
|
||||||
move |this: &mut Self, _input, event, window, cx| {
|
move |this: &mut Self, _input, event, window, cx| {
|
||||||
match event {
|
if let InputEvent::PressEnter { .. } = event {
|
||||||
InputEvent::PressEnter { .. } => {
|
|
||||||
this.send_message(window, cx);
|
this.send_message(window, cx);
|
||||||
}
|
|
||||||
InputEvent::Change(_) => {
|
|
||||||
// this.mention_popup(text, input, cx);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -424,7 +424,7 @@ impl Compose {
|
|||||||
impl Render for Compose {
|
impl Render for Compose {
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let error = self.error_message.read(cx).as_ref();
|
let error = self.error_message.read(cx).as_ref();
|
||||||
let loading = self.user_input.read(cx).loading(cx);
|
let loading = self.user_input.read(cx).loading;
|
||||||
let contacts = self.contacts.read(cx);
|
let contacts = self.contacts.read(cx);
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ impl Render for Preferences {
|
|||||||
.on_click(move |_, _window, cx| {
|
.on_click(move |_, _window, cx| {
|
||||||
if let Some(input) = input_state.upgrade() {
|
if let Some(input) = input_state.upgrade() {
|
||||||
let Ok(url) =
|
let Ok(url) =
|
||||||
Url::parse(input.read(cx).value())
|
Url::parse(&input.read(cx).value())
|
||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -100,12 +100,12 @@ impl Sidebar {
|
|||||||
|
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
// Subscribe for find input events
|
// Subscribe for find input events
|
||||||
cx.subscribe_in(&find_input, window, |this, _state, event, window, cx| {
|
cx.subscribe_in(&find_input, window, |this, state, event, window, cx| {
|
||||||
match event {
|
match event {
|
||||||
InputEvent::PressEnter { .. } => this.search(window, cx),
|
InputEvent::PressEnter { .. } => this.search(window, cx),
|
||||||
InputEvent::Change(text) => {
|
InputEvent::Change => {
|
||||||
// Clear the result when input is empty
|
// Clear the result when input is empty
|
||||||
if text.is_empty() {
|
if state.read(cx).value().is_empty() {
|
||||||
this.clear_search_results(window, cx);
|
this.clear_search_results(window, cx);
|
||||||
} else {
|
} else {
|
||||||
// Run debounced search
|
// Run debounced search
|
||||||
@@ -722,6 +722,7 @@ impl Render for Sidebar {
|
|||||||
.small()
|
.small()
|
||||||
.cleanable()
|
.cleanable()
|
||||||
.appearance(true)
|
.appearance(true)
|
||||||
|
.text_xs()
|
||||||
.suffix(
|
.suffix(
|
||||||
Button::new("find")
|
Button::new("find")
|
||||||
.icon(IconName::Search)
|
.icon(IconName::Search)
|
||||||
|
|||||||
@@ -28,3 +28,6 @@ uuid = "1.10"
|
|||||||
once_cell = "1.19.0"
|
once_cell = "1.19.0"
|
||||||
image = "0.25.1"
|
image = "0.25.1"
|
||||||
linkify = "0.10.0"
|
linkify = "0.10.0"
|
||||||
|
lsp-types = "0.97.0"
|
||||||
|
rope = { git = "https://github.com/zed-industries/zed.git" }
|
||||||
|
sum_tree = { git = "https://github.com/zed-industries/zed.git" }
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ pub struct History<I: HistoryItem> {
|
|||||||
redos: Vec<I>,
|
redos: Vec<I>,
|
||||||
last_changed_at: Instant,
|
last_changed_at: Instant,
|
||||||
version: usize,
|
version: usize,
|
||||||
pub(crate) ignore: bool,
|
|
||||||
max_undo: usize,
|
max_undo: usize,
|
||||||
group_interval: Option<Duration>,
|
group_interval: Option<Duration>,
|
||||||
unique: bool,
|
unique: bool,
|
||||||
|
pub ignore: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<I> History<I>
|
impl<I> History<I>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use gpui::{Context, Timer};
|
use gpui::{px, Context, Pixels, Timer};
|
||||||
|
|
||||||
static INTERVAL: Duration = Duration::from_millis(500);
|
static INTERVAL: Duration = Duration::from_millis(500);
|
||||||
static PAUSE_DELAY: Duration = Duration::from_millis(300);
|
static PAUSE_DELAY: Duration = Duration::from_millis(300);
|
||||||
|
pub(super) const CURSOR_WIDTH: Pixels = px(1.5);
|
||||||
|
|
||||||
/// To manage the Input cursor blinking.
|
/// To manage the Input cursor blinking.
|
||||||
///
|
///
|
||||||
@@ -11,7 +12,7 @@ static PAUSE_DELAY: Duration = Duration::from_millis(300);
|
|||||||
/// Every loop will notify the view to update the `visible`, and Input will observe this update to touch repaint.
|
/// Every loop will notify the view to update the `visible`, and Input will observe this update to touch repaint.
|
||||||
///
|
///
|
||||||
/// The input painter will check if this in visible state, then it will draw the cursor.
|
/// The input painter will check if this in visible state, then it will draw the cursor.
|
||||||
pub(crate) struct BlinkCursor {
|
pub struct BlinkCursor {
|
||||||
visible: bool,
|
visible: bool,
|
||||||
paused: bool,
|
paused: bool,
|
||||||
epoch: usize,
|
epoch: usize,
|
||||||
@@ -52,10 +53,8 @@ impl BlinkCursor {
|
|||||||
|
|
||||||
// Schedule the next blink
|
// Schedule the next blink
|
||||||
let epoch = self.next_epoch();
|
let epoch = self.next_epoch();
|
||||||
|
|
||||||
cx.spawn(async move |this, cx| {
|
cx.spawn(async move |this, cx| {
|
||||||
Timer::after(INTERVAL).await;
|
Timer::after(INTERVAL).await;
|
||||||
|
|
||||||
if let Some(this) = this.upgrade() {
|
if let Some(this) = this.upgrade() {
|
||||||
this.update(cx, |this, cx| this.blink(epoch, cx)).ok();
|
this.update(cx, |this, cx| this.blink(epoch, cx)).ok();
|
||||||
}
|
}
|
||||||
@@ -71,11 +70,11 @@ impl BlinkCursor {
|
|||||||
/// Pause the blinking, and delay 500ms to resume the blinking.
|
/// Pause the blinking, and delay 500ms to resume the blinking.
|
||||||
pub fn pause(&mut self, cx: &mut Context<Self>) {
|
pub fn pause(&mut self, cx: &mut Context<Self>) {
|
||||||
self.paused = true;
|
self.paused = true;
|
||||||
|
self.visible = true;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|
||||||
// delay 500ms to start the blinking
|
// delay 500ms to start the blinking
|
||||||
let epoch = self.next_epoch();
|
let epoch = self.next_epoch();
|
||||||
|
|
||||||
cx.spawn(async move |this, cx| {
|
cx.spawn(async move |this, cx| {
|
||||||
Timer::after(PAUSE_DELAY).await;
|
Timer::after(PAUSE_DELAY).await;
|
||||||
|
|
||||||
@@ -90,3 +89,9 @@ impl BlinkCursor {
|
|||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for BlinkCursor {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::ops::Range;
|
|
||||||
|
|
||||||
use crate::history::HistoryItem;
|
use crate::history::HistoryItem;
|
||||||
|
use crate::input::cursor::Selection;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
pub struct Change {
|
pub struct Change {
|
||||||
pub(crate) old_range: Range<usize>,
|
pub(crate) old_range: Selection,
|
||||||
pub(crate) old_text: String,
|
pub(crate) old_text: String,
|
||||||
pub(crate) new_range: Range<usize>,
|
pub(crate) new_range: Selection,
|
||||||
pub(crate) new_text: String,
|
pub(crate) new_text: String,
|
||||||
version: usize,
|
version: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Change {
|
impl Change {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
old_range: Range<usize>,
|
old_range: impl Into<Selection>,
|
||||||
old_text: &str,
|
old_text: &str,
|
||||||
new_range: Range<usize>,
|
new_range: impl Into<Selection>,
|
||||||
new_text: &str,
|
new_text: &str,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
old_range,
|
old_range: old_range.into(),
|
||||||
old_text: old_text.to_string(),
|
old_text: old_text.to_string(),
|
||||||
new_range,
|
new_range: new_range.into(),
|
||||||
new_text: new_text.to_string(),
|
new_text: new_text.to_string(),
|
||||||
version: 0,
|
version: 0,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
use gpui::{App, Styled};
|
use gpui::{App, Styled};
|
||||||
use i18n::t;
|
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
use crate::button::{Button, ButtonVariants as _};
|
use crate::button::{Button, ButtonVariants};
|
||||||
use crate::{Icon, IconName, Sizable as _};
|
use crate::{Icon, IconName, Sizable};
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn clear_button(cx: &App) -> Button {
|
pub(crate) fn clear_button(cx: &App) -> Button {
|
||||||
Button::new("clean")
|
Button::new("clean")
|
||||||
.icon(Icon::new(IconName::CloseCircle))
|
.icon(Icon::new(IconName::CloseCircle))
|
||||||
.tooltip(t!("common.clear"))
|
.tooltip("Clear")
|
||||||
.small()
|
.small()
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.transparent()
|
.transparent()
|
||||||
|
.text_color(cx.theme().text_muted)
|
||||||
}
|
}
|
||||||
|
|||||||
46
crates/ui/src/input/cursor.rs
Normal file
46
crates/ui/src/input/cursor.rs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
|
/// A selection in the text, represented by start and end byte indices.
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
|
||||||
|
pub struct Selection {
|
||||||
|
pub start: usize,
|
||||||
|
pub end: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Selection {
|
||||||
|
pub fn new(start: usize, end: usize) -> Self {
|
||||||
|
Self { start, end }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.end.saturating_sub(self.start)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.start == self.end
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears the selection, setting start and end to 0.
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.start = 0;
|
||||||
|
self.end = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if the given offset is within the selection range.
|
||||||
|
pub fn contains(&self, offset: usize) -> bool {
|
||||||
|
offset >= self.start && offset < self.end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Range<usize>> for Selection {
|
||||||
|
fn from(value: Range<usize>) -> Self {
|
||||||
|
Self::new(value.start, value.end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<Selection> for Range<usize> {
|
||||||
|
fn from(value: Selection) -> Self {
|
||||||
|
value.start..value.end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Position = lsp_types::Position;
|
||||||
@@ -1,29 +1,34 @@
|
|||||||
use std::{ops::Range, rc::Rc};
|
use std::ops::Range;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
fill, point, px, relative, size, App, Bounds, Corners, Element, ElementId, ElementInputHandler,
|
fill, point, px, relative, size, App, Bounds, Corners, Element, ElementId, ElementInputHandler,
|
||||||
Entity, GlobalElementId, IntoElement, LayoutId, MouseButton, MouseMoveEvent, Path, Pixels,
|
Entity, GlobalElementId, Half, Hitbox, IntoElement, LayoutId, MouseButton, MouseMoveEvent,
|
||||||
Point, SharedString, Size, Style, TextAlign, TextRun, UnderlineStyle, Window, WrappedLine,
|
Path, Pixels, Point, ShapedLine, SharedString, Size, Style, TextAlign, TextRun, UnderlineStyle,
|
||||||
|
Window,
|
||||||
};
|
};
|
||||||
|
use rope::Rope;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
use super::{InputState, LastLayout};
|
use super::blink_cursor::CURSOR_WIDTH;
|
||||||
|
use super::rope_ext::RopeExt;
|
||||||
|
use super::state::{InputState, LastLayout};
|
||||||
use crate::Root;
|
use crate::Root;
|
||||||
|
|
||||||
const CURSOR_THICKNESS: Pixels = px(2.);
|
const BOTTOM_MARGIN_ROWS: usize = 3;
|
||||||
const RIGHT_MARGIN: Pixels = px(5.);
|
pub(super) const RIGHT_MARGIN: Pixels = px(10.);
|
||||||
const BOTTOM_MARGIN_ROWS: usize = 1;
|
pub(super) const LINE_NUMBER_RIGHT_MARGIN: Pixels = px(10.);
|
||||||
|
|
||||||
pub(super) struct TextElement {
|
pub(super) struct TextElement {
|
||||||
input: Entity<InputState>,
|
pub(crate) state: Entity<InputState>,
|
||||||
placeholder: SharedString,
|
placeholder: SharedString,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextElement {
|
impl TextElement {
|
||||||
pub(super) fn new(input: Entity<InputState>) -> Self {
|
pub(super) fn new(state: Entity<InputState>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
input,
|
state,
|
||||||
placeholder: SharedString::default(),
|
placeholder: SharedString::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,12 +41,12 @@ impl TextElement {
|
|||||||
|
|
||||||
fn paint_mouse_listeners(&mut self, window: &mut Window, _: &mut App) {
|
fn paint_mouse_listeners(&mut self, window: &mut Window, _: &mut App) {
|
||||||
window.on_mouse_event({
|
window.on_mouse_event({
|
||||||
let input = self.input.clone();
|
let state = self.state.clone();
|
||||||
|
|
||||||
move |event: &MouseMoveEvent, _, window, cx| {
|
move |event: &MouseMoveEvent, _, window, cx| {
|
||||||
if event.pressed_button == Some(MouseButton::Left) {
|
if event.pressed_button == Some(MouseButton::Left) {
|
||||||
input.update(cx, |input, cx| {
|
state.update(cx, |state, cx| {
|
||||||
input.on_drag_move(event, window, cx);
|
state.on_drag_move(event, window, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,33 +57,44 @@ impl TextElement {
|
|||||||
///
|
///
|
||||||
/// - cursor bounds
|
/// - cursor bounds
|
||||||
/// - scroll offset
|
/// - scroll offset
|
||||||
/// - current line index
|
/// - current row index (No only the visible lines, but all lines)
|
||||||
|
///
|
||||||
|
/// This method also will update for track scroll to cursor.
|
||||||
fn layout_cursor(
|
fn layout_cursor(
|
||||||
&self,
|
&self,
|
||||||
lines: &[WrappedLine],
|
last_layout: &LastLayout,
|
||||||
line_height: Pixels,
|
|
||||||
bounds: &mut Bounds<Pixels>,
|
bounds: &mut Bounds<Pixels>,
|
||||||
line_number_width: Pixels,
|
_: &mut Window,
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> (Option<Bounds<Pixels>>, Point<Pixels>, Option<usize>) {
|
) -> (Option<Bounds<Pixels>>, Point<Pixels>, Option<usize>) {
|
||||||
let input = self.input.read(cx);
|
let state = self.state.read(cx);
|
||||||
let mut selected_range = input.selected_range.clone();
|
|
||||||
if let Some(marked_range) = &input.marked_range {
|
let line_height = last_layout.line_height;
|
||||||
selected_range = marked_range.end..marked_range.end;
|
let visible_range = &last_layout.visible_range;
|
||||||
|
let lines = &last_layout.lines;
|
||||||
|
let text_wrapper = &state.text_wrapper;
|
||||||
|
let line_number_width = last_layout.line_number_width;
|
||||||
|
|
||||||
|
let mut selected_range = state.selected_range;
|
||||||
|
if let Some(ime_marked_range) = &state.ime_marked_range {
|
||||||
|
selected_range = (ime_marked_range.end..ime_marked_range.end).into();
|
||||||
}
|
}
|
||||||
|
|
||||||
let cursor_offset = input.cursor_offset();
|
let cursor = state.cursor();
|
||||||
let mut current_line_index = None;
|
let mut current_row = None;
|
||||||
let mut scroll_offset = input.scroll_handle.offset();
|
let mut scroll_offset = state.scroll_handle.offset();
|
||||||
let mut cursor_bounds = None;
|
let mut cursor_bounds = None;
|
||||||
|
|
||||||
// If the input has a fixed height (Otherwise is auto-grow), we need to add a bottom margin to the input.
|
// 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.is_auto_grow() {
|
let top_bottom_margin = if state.mode.is_auto_grow() {
|
||||||
px(0.) + line_height
|
#[allow(clippy::if_same_then_else)]
|
||||||
|
line_height
|
||||||
|
} else if visible_range.len() < BOTTOM_MARGIN_ROWS * 8 {
|
||||||
|
line_height
|
||||||
} else {
|
} else {
|
||||||
BOTTOM_MARGIN_ROWS * line_height + line_height
|
BOTTOM_MARGIN_ROWS * line_height
|
||||||
};
|
};
|
||||||
|
|
||||||
// The cursor corresponds to the current cursor position in the text no only the line.
|
// The cursor corresponds to the current cursor position in the text no only the line.
|
||||||
let mut cursor_pos = None;
|
let mut cursor_pos = None;
|
||||||
let mut cursor_start = None;
|
let mut cursor_start = None;
|
||||||
@@ -86,17 +102,26 @@ impl TextElement {
|
|||||||
|
|
||||||
let mut prev_lines_offset = 0;
|
let mut prev_lines_offset = 0;
|
||||||
let mut offset_y = px(0.);
|
let mut offset_y = px(0.);
|
||||||
for (line_ix, line) in lines.iter().enumerate() {
|
|
||||||
|
for (ix, wrap_line) in text_wrapper.lines.iter().enumerate() {
|
||||||
|
let row = ix;
|
||||||
|
let line_origin = point(px(0.), offset_y);
|
||||||
|
|
||||||
// break loop if all cursor positions are found
|
// break loop if all cursor positions are found
|
||||||
if cursor_pos.is_some() && cursor_start.is_some() && cursor_end.is_some() {
|
if cursor_pos.is_some() && cursor_start.is_some() && cursor_end.is_some() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
let line_origin = point(px(0.), offset_y);
|
let in_visible_range = ix >= visible_range.start;
|
||||||
|
if let Some(line) = in_visible_range
|
||||||
|
.then(|| lines.get(ix.saturating_sub(visible_range.start)))
|
||||||
|
.flatten()
|
||||||
|
{
|
||||||
|
// If in visible range lines
|
||||||
if cursor_pos.is_none() {
|
if cursor_pos.is_none() {
|
||||||
let offset = cursor_offset.saturating_sub(prev_lines_offset);
|
let offset = cursor.saturating_sub(prev_lines_offset);
|
||||||
if let Some(pos) = line.position_for_index(offset, line_height) {
|
if let Some(pos) = line.position_for_index(offset, line_height) {
|
||||||
current_line_index = Some(line_ix);
|
current_row = Some(row);
|
||||||
cursor_pos = Some(line_origin + pos);
|
cursor_pos = Some(line_origin + pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,40 +139,61 @@ impl TextElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
offset_y += line.size(line_height).height;
|
offset_y += line.size(line_height).height;
|
||||||
// +1 for skip the last `\n`
|
// +1 for the last `\n`
|
||||||
prev_lines_offset += line.len() + 1;
|
prev_lines_offset += line.len() + 1;
|
||||||
|
} else {
|
||||||
|
// If not in the visible range.
|
||||||
|
|
||||||
|
// Just increase the offset_y and prev_lines_offset.
|
||||||
|
// This will let the scroll_offset to track the cursor position correctly.
|
||||||
|
if prev_lines_offset >= cursor && cursor_pos.is_none() {
|
||||||
|
current_row = Some(row);
|
||||||
|
cursor_pos = Some(line_origin);
|
||||||
|
}
|
||||||
|
if prev_lines_offset >= selected_range.start && cursor_start.is_none() {
|
||||||
|
cursor_start = Some(line_origin);
|
||||||
|
}
|
||||||
|
if prev_lines_offset >= selected_range.end && cursor_end.is_none() {
|
||||||
|
cursor_end = Some(line_origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
offset_y += wrap_line.height(line_height);
|
||||||
|
// +1 for the last `\n`
|
||||||
|
prev_lines_offset += wrap_line.len() + 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let (Some(cursor_pos), Some(cursor_start), Some(cursor_end)) =
|
if let (Some(cursor_pos), Some(cursor_start), Some(cursor_end)) =
|
||||||
(cursor_pos, cursor_start, cursor_end)
|
(cursor_pos, cursor_start, cursor_end)
|
||||||
{
|
{
|
||||||
let cursor_moved = input.last_cursor_offset != Some(cursor_offset);
|
let selection_changed = state.last_selected_range != Some(selected_range);
|
||||||
let selection_changed = input.last_selected_range != Some(selected_range.clone());
|
if selection_changed {
|
||||||
|
scroll_offset.x = if scroll_offset.x + cursor_pos.x
|
||||||
if cursor_moved || selection_changed {
|
> (bounds.size.width - line_number_width - RIGHT_MARGIN)
|
||||||
scroll_offset.x =
|
{
|
||||||
if scroll_offset.x + cursor_pos.x > (bounds.size.width - RIGHT_MARGIN) {
|
|
||||||
// cursor is out of right
|
// cursor is out of right
|
||||||
bounds.size.width - RIGHT_MARGIN - cursor_pos.x
|
bounds.size.width - line_number_width - RIGHT_MARGIN - cursor_pos.x
|
||||||
} else if scroll_offset.x + cursor_pos.x < px(0.) {
|
} else if scroll_offset.x + cursor_pos.x < px(0.) {
|
||||||
// cursor is out of left
|
// cursor is out of left
|
||||||
scroll_offset.x - cursor_pos.x
|
scroll_offset.x - cursor_pos.x
|
||||||
} else {
|
} else {
|
||||||
scroll_offset.x
|
scroll_offset.x
|
||||||
};
|
};
|
||||||
scroll_offset.y = if scroll_offset.y + cursor_pos.y + line_height
|
|
||||||
> bounds.size.height - bottom_margin
|
// If we change the scroll_offset.y, GPUI will render and trigger the next run loop.
|
||||||
{
|
// So, here we just adjust offset by `line_height` for move smooth.
|
||||||
|
scroll_offset.y =
|
||||||
|
if scroll_offset.y + cursor_pos.y > bounds.size.height - top_bottom_margin {
|
||||||
// cursor is out of bottom
|
// cursor is out of bottom
|
||||||
bounds.size.height - bottom_margin - cursor_pos.y
|
scroll_offset.y - line_height
|
||||||
} else if scroll_offset.y + cursor_pos.y < px(0.) {
|
} else if scroll_offset.y + cursor_pos.y < top_bottom_margin {
|
||||||
// cursor is out of top
|
// cursor is out of top
|
||||||
scroll_offset.y - cursor_pos.y
|
(scroll_offset.y + line_height).min(px(0.))
|
||||||
} else {
|
} else {
|
||||||
scroll_offset.y
|
scroll_offset.y
|
||||||
};
|
};
|
||||||
|
|
||||||
if input.selection_reversed {
|
if state.selection_reversed {
|
||||||
if scroll_offset.x + cursor_start.x < px(0.) {
|
if scroll_offset.x + cursor_start.x < px(0.) {
|
||||||
// selection start is out of left
|
// selection start is out of left
|
||||||
scroll_offset.x = -cursor_start.x;
|
scroll_offset.x = -cursor_start.x;
|
||||||
@@ -168,54 +214,55 @@ impl TextElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if input.show_cursor(window, cx) {
|
// cursor bounds
|
||||||
// cursor blink
|
|
||||||
let cursor_height = line_height;
|
let cursor_height = line_height;
|
||||||
cursor_bounds = Some(Bounds::new(
|
cursor_bounds = Some(Bounds::new(
|
||||||
point(
|
point(
|
||||||
bounds.left() + cursor_pos.x + line_number_width + scroll_offset.x,
|
bounds.left() + cursor_pos.x + line_number_width + scroll_offset.x,
|
||||||
bounds.top() + cursor_pos.y + ((line_height - cursor_height) / 2.),
|
bounds.top() + cursor_pos.y + ((line_height - cursor_height) / 2.),
|
||||||
),
|
),
|
||||||
size(CURSOR_THICKNESS, cursor_height),
|
size(CURSOR_WIDTH, cursor_height),
|
||||||
));
|
));
|
||||||
};
|
}
|
||||||
|
|
||||||
|
if let Some(deferred_scroll_offset) = state.deferred_scroll_offset {
|
||||||
|
scroll_offset = deferred_scroll_offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
bounds.origin += scroll_offset;
|
bounds.origin += scroll_offset;
|
||||||
|
|
||||||
(cursor_bounds, scroll_offset, current_line_index)
|
(cursor_bounds, scroll_offset, current_row)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn layout_selections(
|
/// Layout the match range to a Path.
|
||||||
&self,
|
pub(crate) fn layout_match_range(
|
||||||
lines: &[WrappedLine],
|
range: Range<usize>,
|
||||||
line_height: Pixels,
|
last_layout: &LastLayout,
|
||||||
bounds: &mut Bounds<Pixels>,
|
bounds: &mut Bounds<Pixels>,
|
||||||
line_number_width: Pixels,
|
|
||||||
_: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> Option<Path<Pixels>> {
|
) -> Option<Path<Pixels>> {
|
||||||
let input = self.input.read(cx);
|
if range.is_empty() {
|
||||||
let mut selected_range = input.selected_range.clone();
|
|
||||||
if let Some(marked_range) = &input.marked_range {
|
|
||||||
if !marked_range.is_empty() {
|
|
||||||
selected_range = marked_range.end..marked_range.end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if selected_range.is_empty() {
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let (start_ix, end_ix) = if selected_range.start < selected_range.end {
|
if range.start < last_layout.visible_range_offset.start
|
||||||
(selected_range.start, selected_range.end)
|
|| range.end > last_layout.visible_range_offset.end
|
||||||
} else {
|
{
|
||||||
(selected_range.end, selected_range.start)
|
return None;
|
||||||
};
|
}
|
||||||
|
|
||||||
let mut prev_lines_offset = 0;
|
let line_height = last_layout.line_height;
|
||||||
|
let visible_top = last_layout.visible_top;
|
||||||
|
let visible_start_offset = last_layout.visible_range_offset.start;
|
||||||
|
let lines = &last_layout.lines;
|
||||||
|
let line_number_width = last_layout.line_number_width;
|
||||||
|
|
||||||
|
let start_ix = range.start;
|
||||||
|
let end_ix = range.end;
|
||||||
|
|
||||||
|
let mut prev_lines_offset = visible_start_offset;
|
||||||
|
let mut offset_y = visible_top;
|
||||||
let mut line_corners = vec![];
|
let mut line_corners = vec![];
|
||||||
|
|
||||||
let mut offset_y = px(0.);
|
|
||||||
for line in lines.iter() {
|
for line in lines.iter() {
|
||||||
let line_size = line.size(line_height);
|
let line_size = line.size(line_height);
|
||||||
let line_wrap_width = line_size.width;
|
let line_wrap_width = line_size.width;
|
||||||
@@ -239,7 +286,6 @@ impl TextElement {
|
|||||||
(end.y / line_height).ceil() as usize - (start.y / line_height).ceil() as usize;
|
(end.y / line_height).ceil() as usize - (start.y / line_height).ceil() as usize;
|
||||||
|
|
||||||
let mut end_x = end.x;
|
let mut end_x = end.x;
|
||||||
|
|
||||||
if wrapped_lines > 0 {
|
if wrapped_lines > 0 {
|
||||||
end_x = line_wrap_width;
|
end_x = line_wrap_width;
|
||||||
}
|
}
|
||||||
@@ -322,39 +368,79 @@ impl TextElement {
|
|||||||
builder.build().ok()
|
builder.build().ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn layout_selections(
|
||||||
|
&self,
|
||||||
|
last_layout: &LastLayout,
|
||||||
|
bounds: &mut Bounds<Pixels>,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Option<Path<Pixels>> {
|
||||||
|
let state = self.state.read(cx);
|
||||||
|
let mut selected_range = state.selected_range;
|
||||||
|
if let Some(ime_marked_range) = &state.ime_marked_range {
|
||||||
|
if !ime_marked_range.is_empty() {
|
||||||
|
selected_range = (ime_marked_range.end..ime_marked_range.end).into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if selected_range.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (start_ix, end_ix) = if selected_range.start < selected_range.end {
|
||||||
|
(selected_range.start, selected_range.end)
|
||||||
|
} else {
|
||||||
|
(selected_range.end, selected_range.start)
|
||||||
|
};
|
||||||
|
|
||||||
|
let range = start_ix.max(last_layout.visible_range_offset.start)
|
||||||
|
..end_ix.min(last_layout.visible_range_offset.end);
|
||||||
|
|
||||||
|
Self::layout_match_range(range, last_layout, bounds)
|
||||||
|
}
|
||||||
|
|
||||||
/// Calculate the visible range of lines in the viewport.
|
/// Calculate the visible range of lines in the viewport.
|
||||||
///
|
///
|
||||||
/// The visible range is based on unwrapped lines (Zero based).
|
/// Returns
|
||||||
|
///
|
||||||
|
/// - visible_range: The visible range is based on unwrapped lines (Zero based).
|
||||||
|
/// - visible_top: The top position of the first visible line in the scroll viewport.
|
||||||
fn calculate_visible_range(
|
fn calculate_visible_range(
|
||||||
&self,
|
&self,
|
||||||
state: &InputState,
|
state: &InputState,
|
||||||
line_height: Pixels,
|
line_height: Pixels,
|
||||||
input_height: Pixels,
|
input_height: Pixels,
|
||||||
) -> Range<usize> {
|
) -> (Range<usize>, Pixels) {
|
||||||
if state.is_single_line() {
|
// Add extra rows to avoid showing empty space when scroll to bottom.
|
||||||
return 0..1;
|
let extra_rows = 1;
|
||||||
|
let mut visible_top = px(0.);
|
||||||
|
if state.mode.is_single_line() {
|
||||||
|
return (0..1, visible_top);
|
||||||
}
|
}
|
||||||
|
|
||||||
let scroll_top = -state.scroll_handle.offset().y;
|
let total_lines = state.text_wrapper.len();
|
||||||
let total_lines = state.text_wrapper.lines.len();
|
let scroll_top = if let Some(deferred_scroll_offset) = state.deferred_scroll_offset {
|
||||||
|
deferred_scroll_offset.y
|
||||||
|
} else {
|
||||||
|
state.scroll_handle.offset().y
|
||||||
|
};
|
||||||
|
|
||||||
let mut visible_range = 0..total_lines;
|
let mut visible_range = 0..total_lines;
|
||||||
let mut line_top = px(0.);
|
let mut line_bottom = px(0.);
|
||||||
|
|
||||||
for (ix, line) in state.text_wrapper.lines.iter().enumerate() {
|
for (ix, line) in state.text_wrapper.lines.iter().enumerate() {
|
||||||
line_top += line.height(line_height);
|
let wrapped_height = line.height(line_height);
|
||||||
|
line_bottom += wrapped_height;
|
||||||
|
|
||||||
if line_top < scroll_top {
|
if line_bottom < -scroll_top {
|
||||||
|
visible_top = line_bottom - wrapped_height;
|
||||||
visible_range.start = ix;
|
visible_range.start = ix;
|
||||||
}
|
}
|
||||||
|
|
||||||
if line_top > scroll_top + input_height {
|
if line_bottom + scroll_top >= input_height {
|
||||||
visible_range.end = (ix + 1).min(total_lines);
|
visible_range.end = (ix + extra_rows).min(total_lines);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
visible_range
|
(visible_range, visible_top)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,13 +448,17 @@ pub(super) struct PrepaintState {
|
|||||||
/// The lines of entire lines.
|
/// The lines of entire lines.
|
||||||
last_layout: LastLayout,
|
last_layout: LastLayout,
|
||||||
/// The lines only contains the visible lines in the viewport, based on `visible_range`.
|
/// The lines only contains the visible lines in the viewport, based on `visible_range`.
|
||||||
line_numbers: Option<Vec<SmallVec<[WrappedLine; 1]>>>,
|
///
|
||||||
line_number_width: Pixels,
|
/// The child is the soft lines.
|
||||||
|
line_numbers: Option<Vec<SmallVec<[ShapedLine; 1]>>>,
|
||||||
/// Size of the scrollable area by entire lines.
|
/// Size of the scrollable area by entire lines.
|
||||||
scroll_size: Size<Pixels>,
|
scroll_size: Size<Pixels>,
|
||||||
cursor_bounds: Option<Bounds<Pixels>>,
|
cursor_bounds: Option<Bounds<Pixels>>,
|
||||||
cursor_scroll_offset: Point<Pixels>,
|
cursor_scroll_offset: Point<Pixels>,
|
||||||
selection_path: Option<Path<Pixels>>,
|
selection_path: Option<Path<Pixels>>,
|
||||||
|
hover_highlight_path: Option<Path<Pixels>>,
|
||||||
|
search_match_paths: Vec<(Path<Pixels>, bool)>,
|
||||||
|
hover_definition_hitbox: Option<Hitbox>,
|
||||||
bounds: Bounds<Pixels>,
|
bounds: Bounds<Pixels>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,34 +470,9 @@ impl IntoElement for TextElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A debug function to print points as SVG path.
|
|
||||||
#[allow(unused)]
|
|
||||||
fn print_points_as_svg_path(line_corners: &Vec<Corners<Point<Pixels>>>, points: &[Point<Pixels>]) {
|
|
||||||
for corners in line_corners {
|
|
||||||
println!(
|
|
||||||
"tl: ({}, {}), tr: ({}, {}), bl: ({}, {}), br: ({}, {})",
|
|
||||||
corners.top_left.x.0 as i32,
|
|
||||||
corners.top_left.y.0 as i32,
|
|
||||||
corners.top_right.x.0 as i32,
|
|
||||||
corners.top_right.y.0 as i32,
|
|
||||||
corners.bottom_left.x.0 as i32,
|
|
||||||
corners.bottom_left.y.0 as i32,
|
|
||||||
corners.bottom_right.x.0 as i32,
|
|
||||||
corners.bottom_right.y.0 as i32,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if !points.is_empty() {
|
|
||||||
println!("M{},{}", points[0].x.0 as i32, points[0].y.0 as i32);
|
|
||||||
for p in points.iter().skip(1) {
|
|
||||||
println!("L{},{}", p.x.0 as i32, p.y.0 as i32);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Element for TextElement {
|
impl Element for TextElement {
|
||||||
type RequestLayoutState = ();
|
|
||||||
type PrepaintState = PrepaintState;
|
type PrepaintState = PrepaintState;
|
||||||
|
type RequestLayoutState = ();
|
||||||
|
|
||||||
fn id(&self) -> Option<ElementId> {
|
fn id(&self) -> Option<ElementId> {
|
||||||
None
|
None
|
||||||
@@ -424,19 +489,20 @@ impl Element for TextElement {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> (LayoutId, Self::RequestLayoutState) {
|
) -> (LayoutId, Self::RequestLayoutState) {
|
||||||
let input = self.input.read(cx);
|
let state = self.state.read(cx);
|
||||||
let line_height = window.line_height();
|
let line_height = window.line_height();
|
||||||
|
|
||||||
let mut style = Style::default();
|
let mut style = Style::default();
|
||||||
style.size.width = relative(1.).into();
|
style.size.width = relative(1.).into();
|
||||||
if self.input.read(cx).is_multi_line() {
|
if state.mode.is_multi_line() {
|
||||||
style.flex_grow = 1.0;
|
style.flex_grow = 1.0;
|
||||||
if let Some(h) = input.mode.height() {
|
|
||||||
style.size.height = h.into();
|
|
||||||
style.min_size.height = line_height.into();
|
|
||||||
} else {
|
|
||||||
style.size.height = relative(1.).into();
|
style.size.height = relative(1.).into();
|
||||||
style.min_size.height = (input.mode.rows() * line_height).into();
|
if state.mode.is_auto_grow() {
|
||||||
|
// Auto grow to let height match to rows, but not exceed max rows.
|
||||||
|
let rows = state.mode.max_rows().min(state.mode.rows());
|
||||||
|
style.min_size.height = (rows * line_height).into();
|
||||||
|
} else {
|
||||||
|
style.min_size.height = line_height.into();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// For single-line inputs, the minimum height should be the line height
|
// For single-line inputs, the minimum height should be the line height
|
||||||
@@ -455,11 +521,19 @@ impl Element for TextElement {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Self::PrepaintState {
|
) -> Self::PrepaintState {
|
||||||
|
let state = self.state.read(cx);
|
||||||
let line_height = window.line_height();
|
let line_height = window.line_height();
|
||||||
let input = self.input.read(cx);
|
|
||||||
let multi_line = input.is_multi_line();
|
let (visible_range, visible_top) =
|
||||||
let visible_range = self.calculate_visible_range(input, line_height, bounds.size.height);
|
self.calculate_visible_range(state, line_height, bounds.size.height);
|
||||||
let text = input.text.clone();
|
let visible_start_offset = state.text.line_start_offset(visible_range.start);
|
||||||
|
let visible_end_offset = state
|
||||||
|
.text
|
||||||
|
.line_end_offset(visible_range.end.saturating_sub(1));
|
||||||
|
|
||||||
|
let state = self.state.read(cx);
|
||||||
|
let multi_line = state.mode.is_multi_line();
|
||||||
|
let text = state.text.clone();
|
||||||
let is_empty = text.is_empty();
|
let is_empty = text.is_empty();
|
||||||
let placeholder = self.placeholder.clone();
|
let placeholder = self.placeholder.clone();
|
||||||
let style = window.text_style();
|
let style = window.text_style();
|
||||||
@@ -467,9 +541,9 @@ impl Element for TextElement {
|
|||||||
let mut bounds = bounds;
|
let mut bounds = bounds;
|
||||||
|
|
||||||
let (display_text, text_color) = if is_empty {
|
let (display_text, text_color) = if is_empty {
|
||||||
(placeholder, cx.theme().text_muted)
|
(Rope::from(placeholder.as_str()), cx.theme().text_muted)
|
||||||
} else if input.masked {
|
} else if state.masked {
|
||||||
("*".repeat(text.chars().count()).into(), cx.theme().text)
|
(Rope::from("*".repeat(text.chars_count())), cx.theme().text)
|
||||||
} else {
|
} else {
|
||||||
(text.clone(), cx.theme().text)
|
(text.clone(), cx.theme().text)
|
||||||
};
|
};
|
||||||
@@ -500,20 +574,20 @@ impl Element for TextElement {
|
|||||||
|
|
||||||
let runs = if !is_empty {
|
let runs = if !is_empty {
|
||||||
vec![run]
|
vec![run]
|
||||||
} else if let Some(marked_range) = &input.marked_range {
|
} else if let Some(ime_marked_range) = &state.ime_marked_range {
|
||||||
// IME marked text
|
// IME marked text
|
||||||
vec![
|
vec![
|
||||||
TextRun {
|
TextRun {
|
||||||
len: marked_range.start,
|
len: ime_marked_range.start,
|
||||||
..run.clone()
|
..run.clone()
|
||||||
},
|
},
|
||||||
TextRun {
|
TextRun {
|
||||||
len: marked_range.end - marked_range.start,
|
len: ime_marked_range.end - ime_marked_range.start,
|
||||||
underline: marked_run.underline,
|
underline: marked_run.underline,
|
||||||
..run.clone()
|
..run.clone()
|
||||||
},
|
},
|
||||||
TextRun {
|
TextRun {
|
||||||
len: display_text.len() - marked_range.end,
|
len: display_text.len() - ime_marked_range.end,
|
||||||
..run.clone()
|
..run.clone()
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -524,35 +598,76 @@ impl Element for TextElement {
|
|||||||
vec![run]
|
vec![run]
|
||||||
};
|
};
|
||||||
|
|
||||||
let wrap_width = if multi_line {
|
let wrap_width = if multi_line && state.soft_wrap {
|
||||||
Some(bounds.size.width - line_number_width - RIGHT_MARGIN)
|
Some(bounds.size.width - line_number_width - RIGHT_MARGIN)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// NOTE: Here 50 lines about 150µs
|
||||||
|
// let measure = crate::Measure::new("shape_text");
|
||||||
|
let visible_text = display_text
|
||||||
|
.slice_rows(visible_range.start as u32..visible_range.end as u32)
|
||||||
|
.to_string();
|
||||||
|
|
||||||
let lines = window
|
let lines = window
|
||||||
.text_system()
|
.text_system()
|
||||||
.shape_text(display_text, font_size, &runs, wrap_width, None)
|
.shape_text(visible_text.into(), font_size, &runs, wrap_width, None)
|
||||||
.expect("failed to shape text");
|
.expect("failed to shape text");
|
||||||
|
// measure.end();
|
||||||
|
|
||||||
let total_wrapped_lines = lines
|
let mut longest_line_width = wrap_width.unwrap_or(px(0.));
|
||||||
.iter()
|
if state.mode.is_multi_line() && !state.soft_wrap && lines.len() > 1 {
|
||||||
.map(|line| {
|
let longtest_line: SharedString = state
|
||||||
// +1 is the first line, `wrap_boundaries` is the wrapped lines after the `\n`.
|
.text
|
||||||
1 + line.wrap_boundaries.len()
|
.line(state.text.summary().longest_row as usize)
|
||||||
})
|
.to_string()
|
||||||
.sum::<usize>();
|
.into();
|
||||||
|
longest_line_width = window
|
||||||
|
.text_system()
|
||||||
|
.shape_line(
|
||||||
|
longtest_line.clone(),
|
||||||
|
font_size,
|
||||||
|
&[TextRun {
|
||||||
|
len: longtest_line.len(),
|
||||||
|
font: style.font(),
|
||||||
|
color: gpui::black(),
|
||||||
|
background_color: None,
|
||||||
|
underline: None,
|
||||||
|
strikethrough: None,
|
||||||
|
}],
|
||||||
|
wrap_width,
|
||||||
|
)
|
||||||
|
.width;
|
||||||
|
}
|
||||||
|
|
||||||
let max_line_width = lines
|
let total_wrapped_lines = state.text_wrapper.len();
|
||||||
.iter()
|
let empty_bottom_height = bounds
|
||||||
.map(|line| line.width())
|
.size
|
||||||
.max()
|
.height
|
||||||
.unwrap_or(bounds.size.width);
|
.half()
|
||||||
|
.max(BOTTOM_MARGIN_ROWS * line_height);
|
||||||
let scroll_size = size(
|
let scroll_size = size(
|
||||||
max_line_width + line_number_width + RIGHT_MARGIN,
|
if longest_line_width + line_number_width + RIGHT_MARGIN > bounds.size.width {
|
||||||
(total_wrapped_lines as f32 * line_height).max(bounds.size.height),
|
longest_line_width + line_number_width + RIGHT_MARGIN
|
||||||
|
} else {
|
||||||
|
longest_line_width
|
||||||
|
},
|
||||||
|
(total_wrapped_lines as f32 * line_height + empty_bottom_height)
|
||||||
|
.max(bounds.size.height),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let mut last_layout = LastLayout {
|
||||||
|
visible_range,
|
||||||
|
visible_top,
|
||||||
|
visible_range_offset: visible_start_offset..visible_end_offset,
|
||||||
|
line_height,
|
||||||
|
wrap_width,
|
||||||
|
line_number_width,
|
||||||
|
lines: Rc::new(lines),
|
||||||
|
cursor_bounds: None,
|
||||||
|
};
|
||||||
|
|
||||||
// `position_for_index` for example
|
// `position_for_index` for example
|
||||||
//
|
//
|
||||||
// #### text
|
// #### text
|
||||||
@@ -584,37 +699,27 @@ impl Element for TextElement {
|
|||||||
|
|
||||||
// Calculate the scroll offset to keep the cursor in view
|
// Calculate the scroll offset to keep the cursor in view
|
||||||
|
|
||||||
let (cursor_bounds, cursor_scroll_offset, _) = self.layout_cursor(
|
let (cursor_bounds, cursor_scroll_offset, _) =
|
||||||
&lines,
|
self.layout_cursor(&last_layout, &mut bounds, window, cx);
|
||||||
line_height,
|
last_layout.cursor_bounds = cursor_bounds;
|
||||||
&mut bounds,
|
|
||||||
line_number_width,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
|
|
||||||
let selection_path = self.layout_selections(
|
let selection_path = self.layout_selections(&last_layout, &mut bounds, cx);
|
||||||
&lines,
|
let search_match_paths = vec![];
|
||||||
line_height,
|
let hover_highlight_path = None;
|
||||||
&mut bounds,
|
let line_numbers = None;
|
||||||
line_number_width,
|
let hover_definition_hitbox = None;
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
|
|
||||||
PrepaintState {
|
PrepaintState {
|
||||||
bounds,
|
bounds,
|
||||||
last_layout: LastLayout {
|
last_layout,
|
||||||
lines: Rc::new(lines),
|
|
||||||
line_height,
|
|
||||||
visible_range,
|
|
||||||
},
|
|
||||||
scroll_size,
|
scroll_size,
|
||||||
line_numbers: None,
|
line_numbers,
|
||||||
line_number_width,
|
|
||||||
cursor_bounds,
|
cursor_bounds,
|
||||||
cursor_scroll_offset,
|
cursor_scroll_offset,
|
||||||
selection_path,
|
selection_path,
|
||||||
|
search_match_paths,
|
||||||
|
hover_highlight_path,
|
||||||
|
hover_definition_hitbox,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -628,21 +733,21 @@ impl Element for TextElement {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) {
|
) {
|
||||||
let focus_handle = self.input.read(cx).focus_handle.clone();
|
let focus_handle = self.state.read(cx).focus_handle.clone();
|
||||||
|
let show_cursor = self.state.read(cx).show_cursor(window, cx);
|
||||||
let focused = focus_handle.is_focused(window);
|
let focused = focus_handle.is_focused(window);
|
||||||
let bounds = prepaint.bounds;
|
let bounds = prepaint.bounds;
|
||||||
let selected_range = self.input.read(cx).selected_range.clone();
|
let selected_range = self.state.read(cx).selected_range;
|
||||||
let visible_range = &prepaint.last_layout.visible_range;
|
|
||||||
|
|
||||||
window.handle_input(
|
window.handle_input(
|
||||||
&focus_handle,
|
&focus_handle,
|
||||||
ElementInputHandler::new(bounds, self.input.clone()),
|
ElementInputHandler::new(bounds, self.state.clone()),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Set Root focused_input when self is focused
|
// Set Root focused_input when self is focused
|
||||||
if focused {
|
if focused {
|
||||||
let state = self.input.clone();
|
let state = self.state.clone();
|
||||||
if Root::read(window, cx).focused_input.as_ref() != Some(&state) {
|
if Root::read(window, cx).focused_input.as_ref() != Some(&state) {
|
||||||
Root::update(window, cx, |root, _, cx| {
|
Root::update(window, cx, |root, _, cx| {
|
||||||
root.focused_input = Some(state);
|
root.focused_input = Some(state);
|
||||||
@@ -653,7 +758,7 @@ impl Element for TextElement {
|
|||||||
|
|
||||||
// And reset focused_input when next_frame start
|
// And reset focused_input when next_frame start
|
||||||
window.on_next_frame({
|
window.on_next_frame({
|
||||||
let state = self.input.clone();
|
let state = self.state.clone();
|
||||||
move |window, cx| {
|
move |window, cx| {
|
||||||
if !focused && Root::read(window, cx).focused_input.as_ref() == Some(&state) {
|
if !focused && Root::read(window, cx).focused_input.as_ref() == Some(&state) {
|
||||||
Root::update(window, cx, |root, _, cx| {
|
Root::update(window, cx, |root, _, cx| {
|
||||||
@@ -668,13 +773,10 @@ impl Element for TextElement {
|
|||||||
let line_height = window.line_height();
|
let line_height = window.line_height();
|
||||||
let origin = bounds.origin;
|
let origin = bounds.origin;
|
||||||
|
|
||||||
let mut invisible_top_padding = px(0.);
|
let invisible_top_padding = prepaint.last_layout.visible_top;
|
||||||
for line in prepaint.last_layout.lines.iter().take(visible_range.start) {
|
|
||||||
invisible_top_padding += line.size(line_height).height;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut mask_offset_y = px(0.);
|
let mut mask_offset_y = px(0.);
|
||||||
if self.input.read(cx).masked {
|
if self.state.read(cx).masked {
|
||||||
// Move down offset for vertical centering the *****
|
// Move down offset for vertical centering the *****
|
||||||
if cfg!(target_os = "macos") {
|
if cfg!(target_os = "macos") {
|
||||||
mask_offset_y = px(3.);
|
mask_offset_y = px(3.);
|
||||||
@@ -683,60 +785,105 @@ impl Element for TextElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Paint active line
|
||||||
let mut offset_y = px(0.);
|
let mut offset_y = px(0.);
|
||||||
if let Some(line_numbers) = prepaint.line_numbers.as_ref() {
|
if let Some(line_numbers) = prepaint.line_numbers.as_ref() {
|
||||||
offset_y += invisible_top_padding;
|
offset_y += invisible_top_padding;
|
||||||
|
|
||||||
// Each item is the normal lines.
|
// Each item is the normal lines.
|
||||||
for lines in line_numbers.iter() {
|
for lines in line_numbers.iter() {
|
||||||
for line in lines {
|
let height = line_height * lines.len() as f32;
|
||||||
let p = point(origin.x, origin.y + offset_y);
|
offset_y += height;
|
||||||
let line_size = line.size(line_height);
|
|
||||||
_ = line.paint(p, line_height, TextAlign::Left, None, window, cx);
|
|
||||||
offset_y += line_size.height;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Paint selections
|
// Paint selections
|
||||||
|
if window.is_window_active() {
|
||||||
|
let secondary_selection = cx.theme().selection;
|
||||||
|
for (path, is_active) in prepaint.search_match_paths.iter() {
|
||||||
|
window.paint_path(path.clone(), secondary_selection);
|
||||||
|
|
||||||
|
if *is_active {
|
||||||
|
window.paint_path(path.clone(), cx.theme().selection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(path) = prepaint.selection_path.take() {
|
if let Some(path) = prepaint.selection_path.take() {
|
||||||
window.paint_path(path, cx.theme().selection);
|
window.paint_path(path, cx.theme().selection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Paint hover highlight
|
||||||
|
if let Some(path) = prepaint.hover_highlight_path.take() {
|
||||||
|
window.paint_path(path, secondary_selection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Paint text
|
// Paint text
|
||||||
let mut offset_y = mask_offset_y + invisible_top_padding;
|
let mut offset_y = mask_offset_y + invisible_top_padding;
|
||||||
for line in prepaint
|
for line in prepaint.last_layout.lines.iter() {
|
||||||
.last_layout
|
let p = point(
|
||||||
.iter()
|
origin.x + prepaint.last_layout.line_number_width,
|
||||||
.skip(visible_range.start)
|
origin.y + offset_y,
|
||||||
.take(visible_range.len())
|
);
|
||||||
{
|
|
||||||
let p = point(origin.x + prepaint.line_number_width, origin.y + offset_y);
|
|
||||||
_ = line.paint(p, line_height, TextAlign::Left, None, window, cx);
|
_ = line.paint(p, line_height, TextAlign::Left, None, window, cx);
|
||||||
offset_y += line.size(line_height).height;
|
offset_y += line.size(line_height).height;
|
||||||
}
|
}
|
||||||
|
|
||||||
if focused {
|
// Paint blinking cursor
|
||||||
|
if focused && show_cursor {
|
||||||
if let Some(mut cursor_bounds) = prepaint.cursor_bounds.take() {
|
if let Some(mut cursor_bounds) = prepaint.cursor_bounds.take() {
|
||||||
cursor_bounds.origin.y += prepaint.cursor_scroll_offset.y;
|
cursor_bounds.origin.y += prepaint.cursor_scroll_offset.y;
|
||||||
window.paint_quad(fill(cursor_bounds, cx.theme().cursor));
|
window.paint_quad(fill(cursor_bounds, cx.theme().cursor));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.input.update(cx, |input, cx| {
|
// Paint line numbers
|
||||||
input.last_layout = Some(prepaint.last_layout.clone());
|
let mut offset_y = px(0.);
|
||||||
input.last_bounds = Some(bounds);
|
if let Some(line_numbers) = prepaint.line_numbers.as_ref() {
|
||||||
input.last_cursor_offset = Some(input.cursor_offset());
|
offset_y += invisible_top_padding;
|
||||||
input.set_input_bounds(input_bounds, cx);
|
|
||||||
input.last_selected_range = Some(selected_range);
|
// Paint line number background
|
||||||
input.scroll_size = prepaint.scroll_size;
|
window.paint_quad(fill(
|
||||||
input.line_number_width = prepaint.line_number_width;
|
Bounds {
|
||||||
input
|
origin: input_bounds.origin,
|
||||||
|
size: size(
|
||||||
|
prepaint.last_layout.line_number_width - LINE_NUMBER_RIGHT_MARGIN,
|
||||||
|
input_bounds.size.height,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
cx.theme().background,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Each item is the normal lines.
|
||||||
|
for lines in line_numbers.iter() {
|
||||||
|
let p = point(input_bounds.origin.x, origin.y + offset_y);
|
||||||
|
|
||||||
|
for line in lines {
|
||||||
|
_ = line.paint(p, line_height, window, cx);
|
||||||
|
offset_y += line_height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.state.update(cx, |state, cx| {
|
||||||
|
state.last_layout = Some(prepaint.last_layout.clone());
|
||||||
|
state.last_bounds = Some(bounds);
|
||||||
|
state.last_cursor = Some(state.cursor());
|
||||||
|
state.set_input_bounds(input_bounds, cx);
|
||||||
|
state.last_selected_range = Some(selected_range);
|
||||||
|
state.scroll_size = prepaint.scroll_size;
|
||||||
|
state
|
||||||
.scroll_handle
|
.scroll_handle
|
||||||
.set_offset(prepaint.cursor_scroll_offset);
|
.set_offset(prepaint.cursor_scroll_offset);
|
||||||
|
state.deferred_scroll_offset = None;
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if let Some(hitbox) = prepaint.hover_definition_hitbox.as_ref() {
|
||||||
|
window.set_cursor_style(gpui::CursorStyle::PointingHand, hitbox);
|
||||||
|
}
|
||||||
|
|
||||||
self.paint_mouse_listeners(window, cx);
|
self.paint_mouse_listeners(window, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ use gpui::SharedString;
|
|||||||
|
|
||||||
#[derive(Clone, PartialEq, Debug)]
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
pub enum MaskToken {
|
pub enum MaskToken {
|
||||||
|
/// 0 Digit, equivalent to `[0]`
|
||||||
|
// Digit0,
|
||||||
/// Digit, equivalent to `[0-9]`
|
/// Digit, equivalent to `[0-9]`
|
||||||
Digit,
|
Digit,
|
||||||
/// Letter, equivalent to `[a-zA-Z]`
|
/// Letter, equivalent to `[a-zA-Z]`
|
||||||
@@ -200,11 +202,25 @@ impl MaskPattern {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if the integer part is valid
|
let sign_positions: Vec<usize> = int_part
|
||||||
if !int_part
|
|
||||||
.chars()
|
.chars()
|
||||||
.all(|ch| ch.is_ascii_digit() || Some(ch) == *separator)
|
.enumerate()
|
||||||
{
|
.filter_map(|(i, ch)| match is_sign(&ch) {
|
||||||
|
true => Some(i),
|
||||||
|
false => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// only one sign is valid
|
||||||
|
// sign is only valid at the beginning of the string
|
||||||
|
if sign_positions.len() > 1 || sign_positions.first() > Some(&0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the integer part is valid
|
||||||
|
if !int_part.chars().enumerate().all(|(i, ch)| {
|
||||||
|
ch.is_ascii_digit() || is_sign(&ch) && i == 0 || Some(ch) == *separator
|
||||||
|
}) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,7 +302,11 @@ impl MaskPattern {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Reverse the integer part for easier grouping
|
// Reverse the integer part for easier grouping
|
||||||
let chars: Vec<char> = int_part.chars().rev().collect();
|
let mut chars: Vec<char> = int_part.chars().rev().collect();
|
||||||
|
|
||||||
|
// Removing the sign from formatting to avoid cases such as: -,123
|
||||||
|
let maybe_signed = chars.iter().position(is_sign).map(|pos| chars.remove(pos));
|
||||||
|
|
||||||
let mut result = String::new();
|
let mut result = String::new();
|
||||||
for (i, ch) in chars.iter().enumerate() {
|
for (i, ch) in chars.iter().enumerate() {
|
||||||
if i > 0 && i % 3 == 0 {
|
if i > 0 && i % 3 == 0 {
|
||||||
@@ -305,6 +325,13 @@ impl MaskPattern {
|
|||||||
} else {
|
} else {
|
||||||
int_with_sep
|
int_with_sep
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let final_str = if let Some(sign) = maybe_signed {
|
||||||
|
format!("{sign}{final_str}")
|
||||||
|
} else {
|
||||||
|
final_str
|
||||||
|
};
|
||||||
|
|
||||||
return final_str.into();
|
return final_str.into();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,3 +403,8 @@ impl MaskPattern {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn is_sign(ch: &char) -> bool {
|
||||||
|
matches!(ch, '+' | '-')
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
mod blink_cursor;
|
mod blink_cursor;
|
||||||
mod change;
|
mod change;
|
||||||
|
mod cursor;
|
||||||
mod element;
|
mod element;
|
||||||
mod mask_pattern;
|
mod mask_pattern;
|
||||||
|
mod mode;
|
||||||
|
mod rope_ext;
|
||||||
mod state;
|
mod state;
|
||||||
mod text_input;
|
mod text_input;
|
||||||
mod text_wrapper;
|
mod text_wrapper;
|
||||||
|
|
||||||
pub(crate) mod clear_button;
|
pub(crate) mod clear_button;
|
||||||
|
|
||||||
#[allow(ambiguous_glob_reexports)]
|
|
||||||
pub use state::*;
|
pub use state::*;
|
||||||
pub use text_input::*;
|
pub use text_input::*;
|
||||||
|
|||||||
129
crates/ui/src/input/mode.rs
Normal file
129
crates/ui/src/input/mode.rs
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
use gpui::SharedString;
|
||||||
|
|
||||||
|
use super::text_wrapper::TextWrapper;
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct TabSize {
|
||||||
|
/// Default is 2
|
||||||
|
pub tab_size: usize,
|
||||||
|
/// Set true to use `\t` as tab indent, default is false
|
||||||
|
pub hard_tabs: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TabSize {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
tab_size: 2,
|
||||||
|
hard_tabs: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TabSize {
|
||||||
|
pub(super) fn to_string(self) -> SharedString {
|
||||||
|
if self.hard_tabs {
|
||||||
|
"\t".into()
|
||||||
|
} else {
|
||||||
|
" ".repeat(self.tab_size).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
pub enum InputMode {
|
||||||
|
#[default]
|
||||||
|
SingleLine,
|
||||||
|
MultiLine {
|
||||||
|
tab: TabSize,
|
||||||
|
rows: usize,
|
||||||
|
},
|
||||||
|
AutoGrow {
|
||||||
|
rows: usize,
|
||||||
|
min_rows: usize,
|
||||||
|
max_rows: usize,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
impl InputMode {
|
||||||
|
#[inline]
|
||||||
|
pub(super) fn is_single_line(&self) -> bool {
|
||||||
|
matches!(self, InputMode::SingleLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(super) fn is_auto_grow(&self) -> bool {
|
||||||
|
matches!(self, InputMode::AutoGrow { .. })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(super) fn is_multi_line(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self,
|
||||||
|
InputMode::MultiLine { .. } | InputMode::AutoGrow { .. }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn set_rows(&mut self, new_rows: usize) {
|
||||||
|
match self {
|
||||||
|
InputMode::MultiLine { rows, .. } => {
|
||||||
|
*rows = new_rows;
|
||||||
|
}
|
||||||
|
InputMode::AutoGrow {
|
||||||
|
rows,
|
||||||
|
min_rows,
|
||||||
|
max_rows,
|
||||||
|
} => {
|
||||||
|
*rows = new_rows.clamp(*min_rows, *max_rows);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn update_auto_grow(&mut self, text_wrapper: &TextWrapper) {
|
||||||
|
if self.is_single_line() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let wrapped_lines = text_wrapper.len();
|
||||||
|
self.set_rows(wrapped_lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// At least 1 row be return.
|
||||||
|
pub(super) fn rows(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
InputMode::MultiLine { rows, .. } => *rows,
|
||||||
|
InputMode::AutoGrow { rows, .. } => *rows,
|
||||||
|
_ => 1,
|
||||||
|
}
|
||||||
|
.max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// At least 1 row be return.
|
||||||
|
#[allow(unused)]
|
||||||
|
pub(super) fn min_rows(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
InputMode::MultiLine { .. } => 1,
|
||||||
|
InputMode::AutoGrow { min_rows, .. } => *min_rows,
|
||||||
|
_ => 1,
|
||||||
|
}
|
||||||
|
.max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
pub(super) fn max_rows(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
InputMode::MultiLine { .. } => usize::MAX,
|
||||||
|
InputMode::AutoGrow { max_rows, .. } => *max_rows,
|
||||||
|
_ => 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(super) fn tab_size(&self) -> Option<&TabSize> {
|
||||||
|
match self {
|
||||||
|
InputMode::MultiLine { tab, .. } => Some(tab),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
207
crates/ui/src/input/rope_ext.rs
Normal file
207
crates/ui/src/input/rope_ext.rs
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
|
use rope::{Point, Rope};
|
||||||
|
|
||||||
|
use super::cursor::Position;
|
||||||
|
|
||||||
|
/// An extension trait for `Rope` to provide additional utility methods.
|
||||||
|
pub trait RopeExt {
|
||||||
|
/// Get the line at the given row (0-based) index, including the `\r` at the end, but not `\n`.
|
||||||
|
///
|
||||||
|
/// Return empty rope if the row (0-based) is out of bounds.
|
||||||
|
fn line(&self, row: usize) -> Rope;
|
||||||
|
|
||||||
|
/// Start offset of the line at the given row (0-based) index.
|
||||||
|
fn line_start_offset(&self, row: usize) -> usize;
|
||||||
|
|
||||||
|
/// Line the end offset (including `\n`) of the line at the given row (0-based) index.
|
||||||
|
///
|
||||||
|
/// Return the end of the rope if the row is out of bounds.
|
||||||
|
fn line_end_offset(&self, row: usize) -> usize;
|
||||||
|
|
||||||
|
/// Return the number of lines in the rope.
|
||||||
|
fn lines_len(&self) -> usize;
|
||||||
|
|
||||||
|
/// Return the lines iterator.
|
||||||
|
///
|
||||||
|
/// Each line is including the `\r` at the end, but not `\n`.
|
||||||
|
fn lines(&self) -> RopeLines;
|
||||||
|
|
||||||
|
/// Check is equal to another rope.
|
||||||
|
fn eq(&self, other: &Rope) -> bool;
|
||||||
|
|
||||||
|
/// Total number of characters in the rope.
|
||||||
|
fn chars_count(&self) -> usize;
|
||||||
|
|
||||||
|
/// Get char at the given offset (byte).
|
||||||
|
///
|
||||||
|
/// If the offset is in the middle of a multi-byte character will panic.
|
||||||
|
///
|
||||||
|
/// If the offset is out of bounds, return None.
|
||||||
|
fn char_at(&self, offset: usize) -> Option<char>;
|
||||||
|
|
||||||
|
/// Get the byte offset from the given line, column [`Position`] (0-based).
|
||||||
|
fn position_to_offset(&self, line_col: &Position) -> usize;
|
||||||
|
|
||||||
|
/// Get the line, column [`Position`] (0-based) from the given byte offset.
|
||||||
|
fn offset_to_position(&self, offset: usize) -> Position;
|
||||||
|
|
||||||
|
/// Get the word byte range at the given offset (byte).
|
||||||
|
fn word_range(&self, offset: usize) -> Option<Range<usize>>;
|
||||||
|
|
||||||
|
/// Get word at the given offset (byte).
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn word_at(&self, offset: usize) -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An iterator over the lines of a `Rope`.
|
||||||
|
pub struct RopeLines {
|
||||||
|
row: usize,
|
||||||
|
end_row: usize,
|
||||||
|
rope: Rope,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RopeLines {
|
||||||
|
/// Create a new `RopeLines` iterator.
|
||||||
|
pub fn new(rope: Rope) -> Self {
|
||||||
|
let end_row = rope.lines_len();
|
||||||
|
Self {
|
||||||
|
row: 0,
|
||||||
|
end_row,
|
||||||
|
rope,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for RopeLines {
|
||||||
|
type Item = Rope;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
if self.row >= self.end_row {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let line = self.rope.line(self.row);
|
||||||
|
self.row += 1;
|
||||||
|
Some(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn nth(&mut self, n: usize) -> Option<Self::Item> {
|
||||||
|
self.row = self.row.saturating_add(n);
|
||||||
|
self.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||||
|
let len = self.end_row - self.row;
|
||||||
|
(len, Some(len))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::iter::ExactSizeIterator for RopeLines {}
|
||||||
|
impl std::iter::FusedIterator for RopeLines {}
|
||||||
|
|
||||||
|
impl RopeExt for Rope {
|
||||||
|
fn line(&self, row: usize) -> Rope {
|
||||||
|
let start = self.line_start_offset(row);
|
||||||
|
let end = start + self.line_len(row as u32) as usize;
|
||||||
|
self.slice(start..end)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn line_start_offset(&self, row: usize) -> usize {
|
||||||
|
let row = row as u32;
|
||||||
|
self.point_to_offset(Point::new(row, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn position_to_offset(&self, pos: &Position) -> usize {
|
||||||
|
let line = self.line(pos.line as usize);
|
||||||
|
self.line_start_offset(pos.line as usize)
|
||||||
|
+ line
|
||||||
|
.chars()
|
||||||
|
.take(pos.character as usize)
|
||||||
|
.map(|c| c.len_utf8())
|
||||||
|
.sum::<usize>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn offset_to_position(&self, offset: usize) -> Position {
|
||||||
|
let point = self.offset_to_point(offset);
|
||||||
|
let line = self.line(point.row as usize);
|
||||||
|
let column = line.clip_offset(point.column as usize, sum_tree::Bias::Left);
|
||||||
|
let character = line.slice(0..column).chars().count();
|
||||||
|
Position::new(point.row, character as u32)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn line_end_offset(&self, row: usize) -> usize {
|
||||||
|
if row > self.max_point().row as usize {
|
||||||
|
return self.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.line_start_offset(row) + self.line_len(row as u32) as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lines_len(&self) -> usize {
|
||||||
|
self.max_point().row as usize + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lines(&self) -> RopeLines {
|
||||||
|
RopeLines::new(self.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eq(&self, other: &Rope) -> bool {
|
||||||
|
self.summary() == other.summary()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chars_count(&self) -> usize {
|
||||||
|
self.chars().count()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn char_at(&self, offset: usize) -> Option<char> {
|
||||||
|
if offset > self.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let offset = self.clip_offset(offset, sum_tree::Bias::Left);
|
||||||
|
self.slice(offset..self.len()).chars().next()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn word_range(&self, offset: usize) -> Option<Range<usize>> {
|
||||||
|
if offset >= self.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let offset = self.clip_offset(offset, sum_tree::Bias::Left);
|
||||||
|
|
||||||
|
let mut left = String::new();
|
||||||
|
for c in self.reversed_chars_at(offset) {
|
||||||
|
if c.is_alphanumeric() || c == '_' {
|
||||||
|
left.insert(0, c);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let start = offset.saturating_sub(left.len());
|
||||||
|
|
||||||
|
let right = self
|
||||||
|
.chars_at(offset)
|
||||||
|
.take_while(|c| c.is_alphanumeric() || *c == '_')
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
let end = offset + right.len();
|
||||||
|
|
||||||
|
if start == end {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(start..end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn word_at(&self, offset: usize) -> String {
|
||||||
|
if let Some(range) = self.word_range(offset) {
|
||||||
|
self.slice(range).to_string()
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -6,11 +6,10 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
use super::InputState;
|
use super::clear_button::clear_button;
|
||||||
use crate::button::{Button, ButtonVariants as _};
|
use super::state::{InputState, CONTEXT};
|
||||||
|
use crate::button::{Button, ButtonVariants};
|
||||||
use crate::indicator::Indicator;
|
use crate::indicator::Indicator;
|
||||||
use crate::input::clear_button::clear_button;
|
|
||||||
use crate::scroll::{Scrollbar, ScrollbarAxis};
|
|
||||||
use crate::{h_flex, IconName, Sizable, Size, StyleSized, StyledExt};
|
use crate::{h_flex, IconName, Sizable, Size, StyleSized, StyledExt};
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
@@ -18,7 +17,6 @@ pub struct TextInput {
|
|||||||
state: Entity<InputState>,
|
state: Entity<InputState>,
|
||||||
style: StyleRefinement,
|
style: StyleRefinement,
|
||||||
size: Size,
|
size: Size,
|
||||||
no_gap: bool,
|
|
||||||
prefix: Option<AnyElement>,
|
prefix: Option<AnyElement>,
|
||||||
suffix: Option<AnyElement>,
|
suffix: Option<AnyElement>,
|
||||||
height: Option<DefiniteLength>,
|
height: Option<DefiniteLength>,
|
||||||
@@ -26,6 +24,8 @@ pub struct TextInput {
|
|||||||
cleanable: bool,
|
cleanable: bool,
|
||||||
mask_toggle: bool,
|
mask_toggle: bool,
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
|
bordered: bool,
|
||||||
|
focus_bordered: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Sizable for TextInput {
|
impl Sizable for TextInput {
|
||||||
@@ -40,9 +40,8 @@ impl TextInput {
|
|||||||
pub fn new(state: &Entity<InputState>) -> Self {
|
pub fn new(state: &Entity<InputState>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
state: state.clone(),
|
state: state.clone(),
|
||||||
style: StyleRefinement::default(),
|
|
||||||
size: Size::default(),
|
size: Size::default(),
|
||||||
no_gap: false,
|
style: StyleRefinement::default(),
|
||||||
prefix: None,
|
prefix: None,
|
||||||
suffix: None,
|
suffix: None,
|
||||||
height: None,
|
height: None,
|
||||||
@@ -50,6 +49,8 @@ impl TextInput {
|
|||||||
cleanable: false,
|
cleanable: false,
|
||||||
mask_toggle: false,
|
mask_toggle: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
bordered: true,
|
||||||
|
focus_bordered: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,12 +76,24 @@ impl TextInput {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the appearance of the input field.
|
/// Set the appearance of the input field, if false the input field will no border, background.
|
||||||
pub fn appearance(mut self, appearance: bool) -> Self {
|
pub fn appearance(mut self, appearance: bool) -> Self {
|
||||||
self.appearance = appearance;
|
self.appearance = appearance;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the bordered for the input, default: true
|
||||||
|
pub fn bordered(mut self, bordered: bool) -> Self {
|
||||||
|
self.bordered = bordered;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set focus border for the input, default is true.
|
||||||
|
pub fn focus_bordered(mut self, bordered: bool) -> Self {
|
||||||
|
self.focus_bordered = bordered;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Set true to show the clear button when the input field is not empty.
|
/// Set true to show the clear button when the input field is not empty.
|
||||||
pub fn cleanable(mut self) -> Self {
|
pub fn cleanable(mut self) -> Self {
|
||||||
self.cleanable = true;
|
self.cleanable = true;
|
||||||
@@ -99,15 +112,6 @@ impl TextInput {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set true to not use gap between input and prefix, suffix, and clear button.
|
|
||||||
///
|
|
||||||
/// Default: false
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(super) fn no_gap(mut self) -> Self {
|
|
||||||
self.no_gap = true;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_toggle_mask_button(state: Entity<InputState>) -> impl IntoElement {
|
fn render_toggle_mask_button(state: Entity<InputState>) -> impl IntoElement {
|
||||||
Button::new("toggle-mask")
|
Button::new("toggle-mask")
|
||||||
.icon(IconName::Eye)
|
.icon(IconName::Eye)
|
||||||
@@ -132,44 +136,51 @@ impl TextInput {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Styled for TextInput {
|
||||||
|
fn style(&mut self) -> &mut StyleRefinement {
|
||||||
|
&mut self.style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl RenderOnce for TextInput {
|
impl RenderOnce for TextInput {
|
||||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
const LINE_HEIGHT: Rems = Rems(1.25);
|
const LINE_HEIGHT: Rems = Rems(1.25);
|
||||||
|
let font = window.text_style().font();
|
||||||
|
let font_size = window.text_style().font_size.to_pixels(window.rem_size());
|
||||||
|
|
||||||
self.state.update(cx, |state, _| {
|
self.state.update(cx, |state, cx| {
|
||||||
state.mode.set_height(self.height);
|
state.text_wrapper.set_font(font, font_size, cx);
|
||||||
state.disabled = self.disabled;
|
state.disabled = self.disabled;
|
||||||
});
|
});
|
||||||
|
|
||||||
let state = self.state.read(cx);
|
let state = self.state.read(cx);
|
||||||
let focused = state.focus_handle.is_focused(window);
|
|
||||||
|
|
||||||
let mut gap_x = match self.size {
|
let gap_x = match self.size {
|
||||||
Size::Small => px(4.),
|
Size::Small => px(4.),
|
||||||
Size::Large => px(8.),
|
Size::Large => px(8.),
|
||||||
_ => px(4.),
|
_ => px(4.),
|
||||||
};
|
};
|
||||||
|
|
||||||
if self.no_gap {
|
|
||||||
gap_x = px(0.);
|
|
||||||
}
|
|
||||||
|
|
||||||
let prefix = self.prefix;
|
|
||||||
let suffix = self.suffix;
|
|
||||||
|
|
||||||
let show_clear_button =
|
|
||||||
self.cleanable && !state.loading && !state.text.is_empty() && state.is_single_line();
|
|
||||||
|
|
||||||
let bg = if state.disabled {
|
let bg = if state.disabled {
|
||||||
cx.theme().surface_background
|
cx.theme().surface_background
|
||||||
} else {
|
} else {
|
||||||
cx.theme().elevated_surface_background
|
cx.theme().elevated_surface_background
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let prefix = self.prefix;
|
||||||
|
let suffix = self.suffix;
|
||||||
|
|
||||||
|
let show_clear_button = self.cleanable
|
||||||
|
&& !state.loading
|
||||||
|
&& !state.text.is_empty()
|
||||||
|
&& state.mode.is_single_line();
|
||||||
|
|
||||||
|
let has_suffix = suffix.is_some() || state.loading || self.mask_toggle || show_clear_button;
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.id(("input", self.state.entity_id()))
|
.id(("input", self.state.entity_id()))
|
||||||
.flex()
|
.flex()
|
||||||
.key_context(crate::input::CONTEXT)
|
.key_context(CONTEXT)
|
||||||
.track_focus(&state.focus_handle)
|
.track_focus(&state.focus_handle)
|
||||||
.when(!state.disabled, |this| {
|
.when(!state.disabled, |this| {
|
||||||
this.on_action(window.listener_for(&self.state, InputState::backspace))
|
this.on_action(window.listener_for(&self.state, InputState::backspace))
|
||||||
@@ -182,17 +193,31 @@ impl RenderOnce for TextInput {
|
|||||||
.on_action(window.listener_for(&self.state, InputState::delete_next_word))
|
.on_action(window.listener_for(&self.state, InputState::delete_next_word))
|
||||||
.on_action(window.listener_for(&self.state, InputState::enter))
|
.on_action(window.listener_for(&self.state, InputState::enter))
|
||||||
.on_action(window.listener_for(&self.state, InputState::escape))
|
.on_action(window.listener_for(&self.state, InputState::escape))
|
||||||
|
.on_action(window.listener_for(&self.state, InputState::paste))
|
||||||
|
.on_action(window.listener_for(&self.state, InputState::cut))
|
||||||
|
.on_action(window.listener_for(&self.state, InputState::undo))
|
||||||
|
.on_action(window.listener_for(&self.state, InputState::redo))
|
||||||
|
.when(state.mode.is_multi_line(), |this| {
|
||||||
|
this.on_action(window.listener_for(&self.state, InputState::indent_inline))
|
||||||
|
.on_action(window.listener_for(&self.state, InputState::outdent_inline))
|
||||||
|
.on_action(window.listener_for(&self.state, InputState::indent_block))
|
||||||
|
.on_action(window.listener_for(&self.state, InputState::outdent_block))
|
||||||
|
.on_action(
|
||||||
|
window.listener_for(&self.state, InputState::shift_to_new_line),
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.on_action(window.listener_for(&self.state, InputState::left))
|
.on_action(window.listener_for(&self.state, InputState::left))
|
||||||
.on_action(window.listener_for(&self.state, InputState::right))
|
.on_action(window.listener_for(&self.state, InputState::right))
|
||||||
.on_action(window.listener_for(&self.state, InputState::select_left))
|
.on_action(window.listener_for(&self.state, InputState::select_left))
|
||||||
.on_action(window.listener_for(&self.state, InputState::select_right))
|
.on_action(window.listener_for(&self.state, InputState::select_right))
|
||||||
.when(state.is_multi_line(), |this| {
|
.when(state.mode.is_multi_line(), |this| {
|
||||||
this.on_action(window.listener_for(&self.state, InputState::up))
|
this.on_action(window.listener_for(&self.state, InputState::up))
|
||||||
.on_action(window.listener_for(&self.state, InputState::down))
|
.on_action(window.listener_for(&self.state, InputState::down))
|
||||||
.on_action(window.listener_for(&self.state, InputState::select_up))
|
.on_action(window.listener_for(&self.state, InputState::select_up))
|
||||||
.on_action(window.listener_for(&self.state, InputState::select_down))
|
.on_action(window.listener_for(&self.state, InputState::select_down))
|
||||||
.on_action(window.listener_for(&self.state, InputState::shift_to_new_line))
|
.on_action(window.listener_for(&self.state, InputState::page_up))
|
||||||
|
.on_action(window.listener_for(&self.state, InputState::page_down))
|
||||||
})
|
})
|
||||||
.on_action(window.listener_for(&self.state, InputState::select_all))
|
.on_action(window.listener_for(&self.state, InputState::select_all))
|
||||||
.on_action(window.listener_for(&self.state, InputState::select_to_start_of_line))
|
.on_action(window.listener_for(&self.state, InputState::select_to_start_of_line))
|
||||||
@@ -209,54 +234,51 @@ impl RenderOnce for TextInput {
|
|||||||
.on_action(window.listener_for(&self.state, InputState::select_to_end))
|
.on_action(window.listener_for(&self.state, InputState::select_to_end))
|
||||||
.on_action(window.listener_for(&self.state, InputState::show_character_palette))
|
.on_action(window.listener_for(&self.state, InputState::show_character_palette))
|
||||||
.on_action(window.listener_for(&self.state, InputState::copy))
|
.on_action(window.listener_for(&self.state, InputState::copy))
|
||||||
.on_action(window.listener_for(&self.state, InputState::paste))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::cut))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::undo))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::redo))
|
|
||||||
.on_key_down(window.listener_for(&self.state, InputState::on_key_down))
|
.on_key_down(window.listener_for(&self.state, InputState::on_key_down))
|
||||||
.on_mouse_down(
|
.on_mouse_down(
|
||||||
MouseButton::Left,
|
MouseButton::Left,
|
||||||
window.listener_for(&self.state, InputState::on_mouse_down),
|
window.listener_for(&self.state, InputState::on_mouse_down),
|
||||||
)
|
)
|
||||||
.on_mouse_down(
|
.on_mouse_down(
|
||||||
MouseButton::Middle,
|
MouseButton::Right,
|
||||||
window.listener_for(&self.state, InputState::on_mouse_down),
|
window.listener_for(&self.state, InputState::on_mouse_down),
|
||||||
)
|
)
|
||||||
.on_mouse_up(
|
.on_mouse_up(
|
||||||
MouseButton::Left,
|
MouseButton::Left,
|
||||||
window.listener_for(&self.state, InputState::on_mouse_up),
|
window.listener_for(&self.state, InputState::on_mouse_up),
|
||||||
)
|
)
|
||||||
|
.on_mouse_up(
|
||||||
|
MouseButton::Right,
|
||||||
|
window.listener_for(&self.state, InputState::on_mouse_up),
|
||||||
|
)
|
||||||
.on_scroll_wheel(window.listener_for(&self.state, InputState::on_scroll_wheel))
|
.on_scroll_wheel(window.listener_for(&self.state, InputState::on_scroll_wheel))
|
||||||
.size_full()
|
.size_full()
|
||||||
.line_height(LINE_HEIGHT)
|
.line_height(LINE_HEIGHT)
|
||||||
.cursor_text()
|
.input_px(self.size)
|
||||||
.input_py(self.size)
|
.input_py(self.size)
|
||||||
.input_h(self.size)
|
.input_h(self.size)
|
||||||
.when(state.is_multi_line(), |this| {
|
.cursor_text()
|
||||||
|
.text_size(font_size)
|
||||||
|
.items_center()
|
||||||
|
.when(state.mode.is_multi_line(), |this| {
|
||||||
this.h_auto()
|
this.h_auto()
|
||||||
.when_some(self.height, |this, height| this.h(height))
|
.when_some(self.height, |this, height| this.h(height))
|
||||||
})
|
})
|
||||||
.when(self.appearance, |this| {
|
.when(self.appearance, |this| {
|
||||||
this.bg(bg)
|
this.bg(bg).rounded(cx.theme().radius)
|
||||||
.rounded(cx.theme().radius)
|
|
||||||
.when(focused, |this| this.border_color(cx.theme().ring))
|
|
||||||
})
|
})
|
||||||
.when(prefix.is_none(), |this| this.input_pl(self.size))
|
|
||||||
.input_pr(self.size)
|
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap(gap_x)
|
.gap(gap_x)
|
||||||
|
.refine_style(&self.style)
|
||||||
.children(prefix)
|
.children(prefix)
|
||||||
// TODO: Define height here, and use it in the input element
|
|
||||||
.child(self.state.clone())
|
.child(self.state.clone())
|
||||||
.child(
|
.when(has_suffix, |this| {
|
||||||
|
this.pr_2().child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.id("suffix")
|
.id("suffix")
|
||||||
.absolute()
|
|
||||||
.gap(gap_x)
|
.gap(gap_x)
|
||||||
.when(self.appearance, |this| this.bg(bg))
|
.when(self.appearance, |this| this.bg(bg))
|
||||||
.items_center()
|
.items_center()
|
||||||
.when(suffix.is_none(), |this| this.pr_1())
|
|
||||||
.right_0()
|
|
||||||
.when(state.loading, |this| {
|
.when(state.loading, |this| {
|
||||||
this.child(Indicator::new().color(cx.theme().text_muted))
|
this.child(Indicator::new().color(cx.theme().text_muted))
|
||||||
})
|
})
|
||||||
@@ -275,24 +297,6 @@ impl RenderOnce for TextInput {
|
|||||||
})
|
})
|
||||||
.children(suffix),
|
.children(suffix),
|
||||||
)
|
)
|
||||||
.when(state.is_multi_line(), |this| {
|
|
||||||
if state.last_layout.is_some() {
|
|
||||||
this.relative().child(
|
|
||||||
div()
|
|
||||||
.absolute()
|
|
||||||
.top_0()
|
|
||||||
.left_0()
|
|
||||||
.right(px(1.))
|
|
||||||
.bottom_0()
|
|
||||||
.child(
|
|
||||||
Scrollbar::vertical(&state.scrollbar_state, &state.scroll_handle)
|
|
||||||
.axis(ScrollbarAxis::Vertical),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
this
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.refine_style(&self.style)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,99 +1,215 @@
|
|||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
||||||
use gpui::{App, Font, LineFragment, Pixels, SharedString};
|
use gpui::{App, Font, LineFragment, Pixels};
|
||||||
|
use rope::Rope;
|
||||||
|
|
||||||
#[allow(unused)]
|
use super::rope_ext::RopeExt;
|
||||||
pub(super) struct LineWrap {
|
|
||||||
/// The number of soft wrapped lines of this line (Not include first line.)
|
/// A line with soft wrapped lines info.
|
||||||
pub(super) wrap_lines: usize,
|
#[derive(Clone)]
|
||||||
/// The range of the line text in the entire text.
|
pub(super) struct LineItem {
|
||||||
pub(super) range: Range<usize>,
|
/// The original line text.
|
||||||
|
line: Rope,
|
||||||
|
/// The soft wrapped lines relative byte range (0..line.len) of this line (Include first line).
|
||||||
|
///
|
||||||
|
/// FIXME: Here in somecase, the `line_wrapper.wrap_line` has returned different
|
||||||
|
/// like the `window.text_system().shape_text`. So, this value may not equal
|
||||||
|
/// the actual rendered lines.
|
||||||
|
wrapped_lines: Vec<Range<usize>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LineWrap {
|
impl LineItem {
|
||||||
|
/// Get the bytes length of this line.
|
||||||
|
#[inline]
|
||||||
|
pub(super) fn len(&self) -> usize {
|
||||||
|
self.line.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get number of soft wrapped lines of this line (include the first line).
|
||||||
|
#[inline]
|
||||||
|
pub(super) fn lines_len(&self) -> usize {
|
||||||
|
self.wrapped_lines.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the height of this line item with given line height.
|
||||||
pub(super) fn height(&self, line_height: Pixels) -> Pixels {
|
pub(super) fn height(&self, line_height: Pixels) -> Pixels {
|
||||||
line_height * (self.wrap_lines + 1)
|
self.lines_len() as f32 * line_height
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Used to prepare the text with soft_wrap to be get lines to displayed in the TextArea
|
/// Used to prepare the text with soft wrap to be get lines to displayed in the Editor.
|
||||||
///
|
///
|
||||||
/// After use lines to calculate the scroll size of the TextArea
|
/// After use lines to calculate the scroll size of the Editor.
|
||||||
pub(super) struct TextWrapper {
|
pub(super) struct TextWrapper {
|
||||||
pub(super) text: SharedString,
|
text: Rope,
|
||||||
/// The wrapped lines, value is start and end index of the line (by split \n).
|
/// Total wrapped lines (Inlucde the first line), value is start and end index of the line.
|
||||||
pub(super) wrapped_lines: Vec<Range<usize>>,
|
soft_lines: usize,
|
||||||
/// The lines by split \n
|
font: Font,
|
||||||
pub(super) lines: Vec<LineWrap>,
|
font_size: Pixels,
|
||||||
pub(super) font: Font,
|
|
||||||
pub(super) font_size: Pixels,
|
|
||||||
/// If is none, it means the text is not wrapped
|
/// If is none, it means the text is not wrapped
|
||||||
pub(super) wrap_width: Option<Pixels>,
|
wrap_width: Option<Pixels>,
|
||||||
|
/// The lines by split \n
|
||||||
|
pub(super) lines: Vec<LineItem>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
impl TextWrapper {
|
impl TextWrapper {
|
||||||
pub(super) fn new(font: Font, font_size: Pixels, wrap_width: Option<Pixels>) -> Self {
|
pub(super) fn new(font: Font, font_size: Pixels, wrap_width: Option<Pixels>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
text: SharedString::default(),
|
text: Rope::new(),
|
||||||
font,
|
font,
|
||||||
font_size,
|
font_size,
|
||||||
wrap_width,
|
wrap_width,
|
||||||
wrapped_lines: Vec::new(),
|
soft_lines: 0,
|
||||||
lines: Vec::new(),
|
lines: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(super) fn set_default_text(&mut self, text: &Rope) {
|
||||||
|
self.text = text.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the total number of lines including wrapped lines.
|
||||||
|
#[inline]
|
||||||
|
pub(super) fn len(&self) -> usize {
|
||||||
|
self.soft_lines
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the line item by row index.
|
||||||
|
#[inline]
|
||||||
|
pub(super) fn line(&self, row: usize) -> Option<&LineItem> {
|
||||||
|
self.lines.get(row)
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn set_wrap_width(&mut self, wrap_width: Option<Pixels>, cx: &mut App) {
|
pub(super) fn set_wrap_width(&mut self, wrap_width: Option<Pixels>, cx: &mut App) {
|
||||||
self.wrap_width = wrap_width;
|
if wrap_width == self.wrap_width {
|
||||||
self.update(&self.text.clone(), true, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn set_font(&mut self, font: Font, font_size: Pixels, cx: &mut App) {
|
|
||||||
self.font = font;
|
|
||||||
self.font_size = font_size;
|
|
||||||
self.update(&self.text.clone(), true, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn update(&mut self, text: &SharedString, force: bool, cx: &mut App) {
|
|
||||||
if &self.text == text && !force {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut wrapped_lines = vec![];
|
self.wrap_width = wrap_width;
|
||||||
let mut lines = vec![];
|
self.update_all(&self.text.clone(), true, cx);
|
||||||
let wrap_width = self.wrap_width.unwrap_or(Pixels::MAX);
|
}
|
||||||
|
|
||||||
|
pub(super) fn set_font(&mut self, font: Font, font_size: Pixels, cx: &mut App) {
|
||||||
|
if self.font.eq(&font) && self.font_size == font_size {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.font = font;
|
||||||
|
self.font_size = font_size;
|
||||||
|
self.update_all(&self.text.clone(), true, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the text wrapper and recalculate the wrapped lines.
|
||||||
|
///
|
||||||
|
/// If the `text` is the same as the current text, do nothing.
|
||||||
|
///
|
||||||
|
/// - `changed_text`: The text [`Rope`] that has changed.
|
||||||
|
/// - `range`: The `selected_range` before change.
|
||||||
|
/// - `new_text`: The inserted text.
|
||||||
|
/// - `force`: Whether to force the update, if false, the update will be skipped if the text is the same.
|
||||||
|
/// - `cx`: The application context.
|
||||||
|
pub(super) fn update(
|
||||||
|
&mut self,
|
||||||
|
changed_text: &Rope,
|
||||||
|
range: &Range<usize>,
|
||||||
|
new_text: &Rope,
|
||||||
|
force: bool,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
let mut line_wrapper = cx
|
let mut line_wrapper = cx
|
||||||
.text_system()
|
.text_system()
|
||||||
.line_wrapper(self.font.clone(), self.font_size);
|
.line_wrapper(self.font.clone(), self.font_size);
|
||||||
|
self._update(
|
||||||
|
changed_text,
|
||||||
|
range,
|
||||||
|
new_text,
|
||||||
|
force,
|
||||||
|
&mut |line_str, wrap_width| {
|
||||||
|
line_wrapper
|
||||||
|
.wrap_line(&[LineFragment::text(line_str)], wrap_width)
|
||||||
|
.collect()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let mut prev_line_ix = 0;
|
fn _update<F>(
|
||||||
for line in text.split('\n') {
|
&mut self,
|
||||||
let mut line_wraps = vec![];
|
changed_text: &Rope,
|
||||||
|
range: &Range<usize>,
|
||||||
|
new_text: &Rope,
|
||||||
|
force: bool,
|
||||||
|
wrap_line: &mut F,
|
||||||
|
) where
|
||||||
|
F: FnMut(&str, Pixels) -> Vec<gpui::Boundary>,
|
||||||
|
{
|
||||||
|
if self.text.eq(changed_text) && !force {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the old changed lines.
|
||||||
|
let start_row = self.text.offset_to_point(range.start).row as usize;
|
||||||
|
let start_row = start_row.min(self.lines.len().saturating_sub(1));
|
||||||
|
let end_row = self.text.offset_to_point(range.end).row as usize;
|
||||||
|
let end_row = end_row.min(self.lines.len().saturating_sub(1));
|
||||||
|
let rows_range = start_row..=end_row;
|
||||||
|
|
||||||
|
// To add the new lines.
|
||||||
|
let new_start_row = changed_text.offset_to_point(range.start).row as usize;
|
||||||
|
let new_start_offset = changed_text.line_start_offset(new_start_row);
|
||||||
|
let new_end_row = changed_text
|
||||||
|
.offset_to_point(range.start + new_text.len())
|
||||||
|
.row as usize;
|
||||||
|
let new_end_offset = changed_text.line_end_offset(new_end_row);
|
||||||
|
let new_range = new_start_offset..new_end_offset;
|
||||||
|
|
||||||
|
let mut new_lines = vec![];
|
||||||
|
|
||||||
|
let wrap_width = self.wrap_width;
|
||||||
|
|
||||||
|
for line in changed_text.slice(new_range).lines() {
|
||||||
|
let line_str = line.to_string();
|
||||||
|
let mut wrapped_lines = vec![];
|
||||||
let mut prev_boundary_ix = 0;
|
let mut prev_boundary_ix = 0;
|
||||||
|
|
||||||
|
// If wrap_width is Pixels::MAX, skip wrapping to disable word wrap
|
||||||
|
if let Some(wrap_width) = wrap_width {
|
||||||
// Here only have wrapped line, if there is no wrap meet, the `line_wraps` result will empty.
|
// Here only have wrapped line, if there is no wrap meet, the `line_wraps` result will empty.
|
||||||
for boundary in line_wrapper.wrap_line(&[LineFragment::text(line)], wrap_width) {
|
for boundary in wrap_line(&line_str, wrap_width) {
|
||||||
line_wraps.push(prev_boundary_ix..boundary.ix);
|
wrapped_lines.push(prev_boundary_ix..boundary.ix);
|
||||||
prev_boundary_ix = boundary.ix;
|
prev_boundary_ix = boundary.ix;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
lines.push(LineWrap {
|
|
||||||
wrap_lines: line_wraps.len(),
|
|
||||||
range: prev_line_ix..prev_line_ix + line.len(),
|
|
||||||
});
|
|
||||||
|
|
||||||
wrapped_lines.extend(line_wraps);
|
|
||||||
// Reset of the line
|
// Reset of the line
|
||||||
if !line[prev_boundary_ix..].is_empty() || prev_boundary_ix == 0 {
|
if !line_str[prev_boundary_ix..].is_empty() || prev_boundary_ix == 0 {
|
||||||
wrapped_lines.push(prev_line_ix + prev_boundary_ix..prev_line_ix + line.len());
|
wrapped_lines.push(prev_boundary_ix..line.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
prev_line_ix += line.len() + 1;
|
new_lines.push(LineItem {
|
||||||
|
line: line.clone(),
|
||||||
|
wrapped_lines,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
self.text = text.clone();
|
// dbg!(&new_lines.len());
|
||||||
self.wrapped_lines = wrapped_lines;
|
// dbg!(self.lines.len());
|
||||||
self.lines = lines;
|
if self.lines.is_empty() {
|
||||||
|
self.lines = new_lines;
|
||||||
|
} else {
|
||||||
|
self.lines.splice(rows_range, new_lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
// dbg!(self.lines.len());
|
||||||
|
self.text = changed_text.clone();
|
||||||
|
self.soft_lines = self.lines.iter().map(|l| l.lines_len()).sum();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the text wrapper and recalculate the wrapped lines.
|
||||||
|
///
|
||||||
|
/// If the `text` is the same as the current text, do nothing.
|
||||||
|
pub(crate) fn update_all(&mut self, text: &Rope, force: bool, cx: &mut App) {
|
||||||
|
self.update(text, &(0..text.len()), text, force, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -299,14 +299,16 @@ where
|
|||||||
|
|
||||||
fn on_query_input_event(
|
fn on_query_input_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &Entity<InputState>,
|
state: &Entity<InputState>,
|
||||||
event: &InputEvent,
|
event: &InputEvent,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
match event {
|
match event {
|
||||||
InputEvent::Change(text) => {
|
InputEvent::Change => {
|
||||||
|
let text = state.read(cx).value();
|
||||||
let text = text.trim().to_string();
|
let text = text.trim().to_string();
|
||||||
|
|
||||||
if Some(&text) == self.last_query.as_ref() {
|
if Some(&text) == self.last_query.as_ref() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -347,7 +349,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_querying(&mut self, querying: bool, _: &mut Window, cx: &mut Context<Self>) {
|
fn set_querying(&mut self, querying: bool, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.querying = querying;
|
self.querying = querying;
|
||||||
if let Some(input) = &self.query_input {
|
if let Some(input) = &self.query_input {
|
||||||
input.update(cx, |input, cx| input.set_loading(querying, cx))
|
input.update(cx, |input, cx| input.set_loading(querying, cx))
|
||||||
|
|||||||
Reference in New Issue
Block a user