chore: improve text input (#94)
* update history * hide cursor & selection when window is deactivated - gpui-component * . * update input to catch up with gpui-component * adjust history
This commit is contained in:
26
Cargo.lock
generated
26
Cargo.lock
generated
@@ -1075,7 +1075,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collections"
|
name = "collections"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#b9ff538747556f3f2f5ed4ee109654eb80cc9c9c"
|
source = "git+https://github.com/zed-industries/zed#ebad5ca50e11fc9d3fbbab397888d0944a825bb7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"rustc-hash 2.1.1",
|
"rustc-hash 2.1.1",
|
||||||
@@ -1476,7 +1476,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#b9ff538747556f3f2f5ed4ee109654eb80cc9c9c"
|
source = "git+https://github.com/zed-industries/zed#ebad5ca50e11fc9d3fbbab397888d0944a825bb7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -2328,7 +2328,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui"
|
name = "gpui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#b9ff538747556f3f2f5ed4ee109654eb80cc9c9c"
|
source = "git+https://github.com/zed-industries/zed#ebad5ca50e11fc9d3fbbab397888d0944a825bb7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"as-raw-xcb-connection",
|
"as-raw-xcb-connection",
|
||||||
@@ -2421,7 +2421,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#b9ff538747556f3f2f5ed4ee109654eb80cc9c9c"
|
source = "git+https://github.com/zed-industries/zed#ebad5ca50e11fc9d3fbbab397888d0944a825bb7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -2433,7 +2433,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#b9ff538747556f3f2f5ed4ee109654eb80cc9c9c"
|
source = "git+https://github.com/zed-industries/zed#ebad5ca50e11fc9d3fbbab397888d0944a825bb7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gpui",
|
"gpui",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -2655,7 +2655,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#b9ff538747556f3f2f5ed4ee109654eb80cc9c9c"
|
source = "git+https://github.com/zed-industries/zed#ebad5ca50e11fc9d3fbbab397888d0944a825bb7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -2672,7 +2672,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#b9ff538747556f3f2f5ed4ee109654eb80cc9c9c"
|
source = "git+https://github.com/zed-industries/zed#ebad5ca50e11fc9d3fbbab397888d0944a825bb7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-platform-verifier",
|
"rustls-platform-verifier",
|
||||||
@@ -3449,7 +3449,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "media"
|
name = "media"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#b9ff538747556f3f2f5ed4ee109654eb80cc9c9c"
|
source = "git+https://github.com/zed-industries/zed#ebad5ca50e11fc9d3fbbab397888d0944a825bb7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bindgen 0.71.1",
|
"bindgen 0.71.1",
|
||||||
@@ -4802,7 +4802,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "refineable"
|
name = "refineable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#b9ff538747556f3f2f5ed4ee109654eb80cc9c9c"
|
source = "git+https://github.com/zed-industries/zed#ebad5ca50e11fc9d3fbbab397888d0944a825bb7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"derive_refineable",
|
"derive_refineable",
|
||||||
"workspace-hack",
|
"workspace-hack",
|
||||||
@@ -4953,7 +4953,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#b9ff538747556f3f2f5ed4ee109654eb80cc9c9c"
|
source = "git+https://github.com/zed-industries/zed#ebad5ca50e11fc9d3fbbab397888d0944a825bb7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -5479,7 +5479,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#b9ff538747556f3f2f5ed4ee109654eb80cc9c9c"
|
source = "git+https://github.com/zed-industries/zed#ebad5ca50e11fc9d3fbbab397888d0944a825bb7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -5864,7 +5864,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#b9ff538747556f3f2f5ed4ee109654eb80cc9c9c"
|
source = "git+https://github.com/zed-industries/zed#ebad5ca50e11fc9d3fbbab397888d0944a825bb7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"log",
|
"log",
|
||||||
@@ -6838,7 +6838,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#b9ff538747556f3f2f5ed4ee109654eb80cc9c9c"
|
source = "git+https://github.com/zed-industries/zed#ebad5ca50e11fc9d3fbbab397888d0944a825bb7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-fs",
|
"async-fs",
|
||||||
|
|||||||
@@ -103,8 +103,6 @@ impl Compose {
|
|||||||
.map(|profile| Contact::new(profile.public_key()))
|
.map(|profile| Contact::new(profile.public_key()))
|
||||||
.collect_vec();
|
.collect_vec();
|
||||||
|
|
||||||
log::info!("get contacts");
|
|
||||||
|
|
||||||
Ok(contacts)
|
Ok(contacts)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
pub trait HistoryItem: Clone {
|
pub trait HistoryItem: Clone + PartialEq {
|
||||||
fn version(&self) -> usize;
|
fn version(&self) -> usize;
|
||||||
fn set_version(&mut self, version: usize);
|
fn set_version(&mut self, version: usize);
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,11 @@ pub trait HistoryItem: Clone {
|
|||||||
/// This is now used in Input for undo/redo operations. You can also use this in
|
/// This is now used in Input for undo/redo operations. You can also use this in
|
||||||
/// your own models to keep track of changes, for example to track the tab
|
/// your own models to keep track of changes, for example to track the tab
|
||||||
/// history for prev/next features.
|
/// history for prev/next features.
|
||||||
|
///
|
||||||
|
/// ## Use cases
|
||||||
|
///
|
||||||
|
/// - Undo/redo operations in Input
|
||||||
|
/// - Tracking tab history for prev/next features
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct History<I: HistoryItem> {
|
pub struct History<I: HistoryItem> {
|
||||||
undos: Vec<I>,
|
undos: Vec<I>,
|
||||||
@@ -20,6 +25,7 @@ pub struct History<I: HistoryItem> {
|
|||||||
pub(crate) ignore: bool,
|
pub(crate) ignore: bool,
|
||||||
max_undo: usize,
|
max_undo: usize,
|
||||||
group_interval: Option<Duration>,
|
group_interval: Option<Duration>,
|
||||||
|
unique: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<I> History<I>
|
impl<I> History<I>
|
||||||
@@ -35,6 +41,7 @@ where
|
|||||||
version: 0,
|
version: 0,
|
||||||
max_undo: 1000,
|
max_undo: 1000,
|
||||||
group_interval: None,
|
group_interval: None,
|
||||||
|
unique: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,6 +51,13 @@ where
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the history to be unique, defaults to false.
|
||||||
|
/// If set to true, the history will only keep unique changes.
|
||||||
|
pub fn unique(mut self) -> Self {
|
||||||
|
self.unique = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the interval in milliseconds to group changes, defaults to None.
|
/// Set the interval in milliseconds to group changes, defaults to None.
|
||||||
pub fn group_interval(mut self, group_interval: Duration) -> Self {
|
pub fn group_interval(mut self, group_interval: Duration) -> Self {
|
||||||
self.group_interval = Some(group_interval);
|
self.group_interval = Some(group_interval);
|
||||||
@@ -73,11 +87,32 @@ where
|
|||||||
self.undos.remove(0);
|
self.undos.remove(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.unique {
|
||||||
|
self.undos.retain(|c| *c != item);
|
||||||
|
self.redos.retain(|c| *c != item);
|
||||||
|
}
|
||||||
|
|
||||||
let mut item = item;
|
let mut item = item;
|
||||||
item.set_version(version);
|
item.set_version(version);
|
||||||
self.undos.push(item);
|
self.undos.push(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the undo stack.
|
||||||
|
pub fn undos(&self) -> &Vec<I> {
|
||||||
|
&self.undos
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the redo stack.
|
||||||
|
pub fn redos(&self) -> &Vec<I> {
|
||||||
|
&self.redos
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear the undo and redo stacks.
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.undos.clear();
|
||||||
|
self.redos.clear();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn undo(&mut self) -> Option<Vec<I>> {
|
pub fn undo(&mut self) -> Option<Vec<I>> {
|
||||||
if let Some(first_change) = self.undos.pop() {
|
if let Some(first_change) = self.undos.pop() {
|
||||||
let mut changes = vec![first_change.clone()];
|
let mut changes = vec![first_change.clone()];
|
||||||
@@ -93,7 +128,7 @@ where
|
|||||||
changes.push(change);
|
changes.push(change);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.redos.extend(changes.iter().rev().cloned());
|
self.redos.extend(changes.clone());
|
||||||
Some(changes)
|
Some(changes)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -114,7 +149,7 @@ where
|
|||||||
let change = self.redos.pop().unwrap();
|
let change = self.redos.pop().unwrap();
|
||||||
changes.push(change);
|
changes.push(change);
|
||||||
}
|
}
|
||||||
self.undos.extend(changes.iter().rev().cloned());
|
self.undos.extend(changes.clone());
|
||||||
Some(changes)
|
Some(changes)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -130,76 +165,3 @@ where
|
|||||||
Self::new()
|
Self::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct TabIndex {
|
|
||||||
tab_index: usize,
|
|
||||||
version: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<usize> for TabIndex {
|
|
||||||
fn from(value: usize) -> Self {
|
|
||||||
TabIndex {
|
|
||||||
tab_index: value,
|
|
||||||
version: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HistoryItem for TabIndex {
|
|
||||||
fn version(&self) -> usize {
|
|
||||||
self.version
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_version(&mut self, version: usize) {
|
|
||||||
self.version = version;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_history() {
|
|
||||||
let mut history: History<TabIndex> = History::new().max_undo(100);
|
|
||||||
history.push(0.into());
|
|
||||||
history.push(3.into());
|
|
||||||
history.push(2.into());
|
|
||||||
history.push(1.into());
|
|
||||||
|
|
||||||
assert_eq!(history.version(), 4);
|
|
||||||
let changes = history.undo().unwrap();
|
|
||||||
assert_eq!(changes.len(), 1);
|
|
||||||
assert_eq!(changes[0].tab_index, 1);
|
|
||||||
|
|
||||||
let changes = history.undo().unwrap();
|
|
||||||
assert_eq!(changes.len(), 1);
|
|
||||||
assert_eq!(changes[0].tab_index, 2);
|
|
||||||
|
|
||||||
history.push(5.into());
|
|
||||||
|
|
||||||
let changes = history.redo().unwrap();
|
|
||||||
assert_eq!(changes[0].tab_index, 2);
|
|
||||||
|
|
||||||
let changes = history.redo().unwrap();
|
|
||||||
assert_eq!(changes[0].tab_index, 1);
|
|
||||||
|
|
||||||
let changes = history.undo().unwrap();
|
|
||||||
assert_eq!(changes[0].tab_index, 1);
|
|
||||||
|
|
||||||
let changes = history.undo().unwrap();
|
|
||||||
assert_eq!(changes[0].tab_index, 2);
|
|
||||||
|
|
||||||
let changes = history.undo().unwrap();
|
|
||||||
assert_eq!(changes[0].tab_index, 5);
|
|
||||||
|
|
||||||
let changes = history.undo().unwrap();
|
|
||||||
assert_eq!(changes[0].tab_index, 3);
|
|
||||||
|
|
||||||
let changes = history.undo().unwrap();
|
|
||||||
assert_eq!(changes[0].tab_index, 0);
|
|
||||||
|
|
||||||
assert!(history.undo().is_none());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
|
use std::{ops::Range, 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, PaintQuad, Path,
|
Entity, GlobalElementId, IntoElement, LayoutId, MouseButton, MouseMoveEvent, Path, Pixels,
|
||||||
Pixels, Point, SharedString, Style, TextAlign, TextRun, UnderlineStyle, Window, WrappedLine,
|
Point, SharedString, Size, Style, TextAlign, TextRun, UnderlineStyle, Window, WrappedLine,
|
||||||
};
|
};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
use super::InputState;
|
use super::{InputState, LastLayout};
|
||||||
use crate::Root;
|
use crate::Root;
|
||||||
|
|
||||||
const CURSOR_THICKNESS: Pixels = px(2.);
|
const CURSOR_THICKNESS: Pixels = px(2.);
|
||||||
@@ -46,19 +48,30 @@ impl TextElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the:
|
||||||
|
///
|
||||||
|
/// - cursor bounds
|
||||||
|
/// - scroll offset
|
||||||
|
/// - current line index
|
||||||
fn layout_cursor(
|
fn layout_cursor(
|
||||||
&self,
|
&self,
|
||||||
lines: &[WrappedLine],
|
lines: &[WrappedLine],
|
||||||
line_height: Pixels,
|
line_height: Pixels,
|
||||||
bounds: &mut Bounds<Pixels>,
|
bounds: &mut Bounds<Pixels>,
|
||||||
|
line_number_width: Pixels,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> (Option<PaintQuad>, Point<Pixels>) {
|
) -> (Option<Bounds<Pixels>>, Point<Pixels>, Option<usize>) {
|
||||||
let input = self.input.read(cx);
|
let input = self.input.read(cx);
|
||||||
let selected_range = &input.selected_range;
|
let mut selected_range = input.selected_range.clone();
|
||||||
|
if let Some(marked_range) = &input.marked_range {
|
||||||
|
selected_range = marked_range.end..marked_range.end;
|
||||||
|
}
|
||||||
|
|
||||||
let cursor_offset = input.cursor_offset();
|
let cursor_offset = input.cursor_offset();
|
||||||
|
let mut current_line_index = None;
|
||||||
let mut scroll_offset = input.scroll_handle.offset();
|
let mut scroll_offset = input.scroll_handle.offset();
|
||||||
let mut cursor = 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 bottom_margin = if input.is_auto_grow() {
|
||||||
@@ -73,7 +86,7 @@ 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 in lines.iter() {
|
for (line_ix, line) in lines.iter().enumerate() {
|
||||||
// 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;
|
||||||
@@ -83,6 +96,7 @@ impl TextElement {
|
|||||||
if cursor_pos.is_none() {
|
if cursor_pos.is_none() {
|
||||||
let offset = cursor_offset.saturating_sub(prev_lines_offset);
|
let offset = cursor_offset.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);
|
||||||
cursor_pos = Some(line_origin + pos);
|
cursor_pos = Some(line_origin + pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,26 +168,22 @@ impl TextElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bounds.origin += scroll_offset;
|
|
||||||
|
|
||||||
if input.show_cursor(window, cx) {
|
if input.show_cursor(window, cx) {
|
||||||
// cursor blink
|
// cursor blink
|
||||||
let cursor_height =
|
let cursor_height = line_height;
|
||||||
window.text_style().font_size.to_pixels(window.rem_size()) + px(2.);
|
cursor_bounds = Some(Bounds::new(
|
||||||
cursor = Some(fill(
|
|
||||||
Bounds::new(
|
|
||||||
point(
|
point(
|
||||||
bounds.left() + cursor_pos.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_THICKNESS, cursor_height),
|
||||||
),
|
));
|
||||||
cx.theme().cursor,
|
|
||||||
))
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
(cursor, scroll_offset)
|
bounds.origin += scroll_offset;
|
||||||
|
|
||||||
|
(cursor_bounds, scroll_offset, current_line_index)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn layout_selections(
|
fn layout_selections(
|
||||||
@@ -181,11 +191,17 @@ impl TextElement {
|
|||||||
lines: &[WrappedLine],
|
lines: &[WrappedLine],
|
||||||
line_height: Pixels,
|
line_height: Pixels,
|
||||||
bounds: &mut Bounds<Pixels>,
|
bounds: &mut Bounds<Pixels>,
|
||||||
|
line_number_width: Pixels,
|
||||||
_: &mut Window,
|
_: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Option<Path<Pixels>> {
|
) -> Option<Path<Pixels>> {
|
||||||
let input = self.input.read(cx);
|
let input = self.input.read(cx);
|
||||||
let selected_range = &input.selected_range;
|
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() {
|
if selected_range.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -295,20 +311,62 @@ impl TextElement {
|
|||||||
|
|
||||||
// print_points_as_svg_path(&line_corners, &points);
|
// print_points_as_svg_path(&line_corners, &points);
|
||||||
|
|
||||||
|
let path_origin = bounds.origin + point(line_number_width, px(0.));
|
||||||
let first_p = *points.first().unwrap();
|
let first_p = *points.first().unwrap();
|
||||||
let mut builder = gpui::PathBuilder::fill();
|
let mut builder = gpui::PathBuilder::fill();
|
||||||
builder.move_to(bounds.origin + first_p);
|
builder.move_to(path_origin + first_p);
|
||||||
for p in points.iter().skip(1) {
|
for p in points.iter().skip(1) {
|
||||||
builder.line_to(bounds.origin + *p);
|
builder.line_to(path_origin + *p);
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.build().ok()
|
builder.build().ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Calculate the visible range of lines in the viewport.
|
||||||
|
///
|
||||||
|
/// The visible range is based on unwrapped lines (Zero based).
|
||||||
|
fn calculate_visible_range(
|
||||||
|
&self,
|
||||||
|
state: &InputState,
|
||||||
|
line_height: Pixels,
|
||||||
|
input_height: Pixels,
|
||||||
|
) -> Range<usize> {
|
||||||
|
if state.is_single_line() {
|
||||||
|
return 0..1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let scroll_top = -state.scroll_handle.offset().y;
|
||||||
|
let total_lines = state.text_wrapper.lines.len();
|
||||||
|
|
||||||
|
let mut visible_range = 0..total_lines;
|
||||||
|
let mut line_top = px(0.);
|
||||||
|
|
||||||
|
for (ix, line) in state.text_wrapper.lines.iter().enumerate() {
|
||||||
|
line_top += line.height(line_height);
|
||||||
|
|
||||||
|
if line_top < scroll_top {
|
||||||
|
visible_range.start = ix;
|
||||||
|
}
|
||||||
|
|
||||||
|
if line_top > scroll_top + input_height {
|
||||||
|
visible_range.end = (ix + 1).min(total_lines);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visible_range
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) struct PrepaintState {
|
pub(super) struct PrepaintState {
|
||||||
lines: SmallVec<[WrappedLine; 1]>,
|
/// The lines of entire lines.
|
||||||
cursor: Option<PaintQuad>,
|
last_layout: LastLayout,
|
||||||
|
/// 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,
|
||||||
|
/// Size of the scrollable area by entire lines.
|
||||||
|
scroll_size: Size<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>>,
|
||||||
bounds: Bounds<Pixels>,
|
bounds: Bounds<Pixels>,
|
||||||
@@ -348,8 +406,8 @@ fn print_points_as_svg_path(line_corners: &Vec<Corners<Point<Pixels>>>, points:
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Element for TextElement {
|
impl Element for TextElement {
|
||||||
type PrepaintState = PrepaintState;
|
|
||||||
type RequestLayoutState = ();
|
type RequestLayoutState = ();
|
||||||
|
type PrepaintState = PrepaintState;
|
||||||
|
|
||||||
fn id(&self) -> Option<ElementId> {
|
fn id(&self) -> Option<ElementId> {
|
||||||
None
|
None
|
||||||
@@ -397,22 +455,27 @@ impl Element for TextElement {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Self::PrepaintState {
|
) -> Self::PrepaintState {
|
||||||
let multi_line = self.input.read(cx).is_multi_line();
|
|
||||||
let line_height = window.line_height();
|
let line_height = window.line_height();
|
||||||
let input = self.input.read(cx);
|
let input = self.input.read(cx);
|
||||||
|
let multi_line = input.is_multi_line();
|
||||||
|
let visible_range = self.calculate_visible_range(input, line_height, bounds.size.height);
|
||||||
let text = input.text.clone();
|
let text = input.text.clone();
|
||||||
|
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();
|
||||||
|
let font_size = style.font_size.to_pixels(window.rem_size());
|
||||||
let mut bounds = bounds;
|
let mut bounds = bounds;
|
||||||
|
|
||||||
let (display_text, text_color) = if text.is_empty() {
|
let (display_text, text_color) = if is_empty {
|
||||||
(placeholder, cx.theme().text_muted)
|
(placeholder, cx.theme().text_muted)
|
||||||
} else if input.masked {
|
} else if input.masked {
|
||||||
("*".repeat(text.chars().count()).into(), cx.theme().text)
|
("*".repeat(text.chars().count()).into(), cx.theme().text)
|
||||||
} else {
|
} else {
|
||||||
(text, cx.theme().text)
|
(text.clone(), cx.theme().text)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let line_number_width = px(0.);
|
||||||
|
|
||||||
let run = TextRun {
|
let run = TextRun {
|
||||||
len: display_text.len(),
|
len: display_text.len(),
|
||||||
font: style.font(),
|
font: style.font(),
|
||||||
@@ -422,7 +485,23 @@ impl Element for TextElement {
|
|||||||
strikethrough: None,
|
strikethrough: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let runs = if let Some(marked_range) = input.marked_range.as_ref() {
|
let marked_run = TextRun {
|
||||||
|
len: 0,
|
||||||
|
font: style.font(),
|
||||||
|
color: text_color,
|
||||||
|
background_color: None,
|
||||||
|
underline: Some(UnderlineStyle {
|
||||||
|
thickness: px(1.),
|
||||||
|
color: Some(text_color),
|
||||||
|
wavy: false,
|
||||||
|
}),
|
||||||
|
strikethrough: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let runs = if !is_empty {
|
||||||
|
vec![run]
|
||||||
|
} else if let Some(marked_range) = &input.marked_range {
|
||||||
|
// IME marked text
|
||||||
vec![
|
vec![
|
||||||
TextRun {
|
TextRun {
|
||||||
len: marked_range.start,
|
len: marked_range.start,
|
||||||
@@ -430,11 +509,7 @@ impl Element for TextElement {
|
|||||||
},
|
},
|
||||||
TextRun {
|
TextRun {
|
||||||
len: marked_range.end - marked_range.start,
|
len: marked_range.end - marked_range.start,
|
||||||
underline: Some(UnderlineStyle {
|
underline: marked_run.underline,
|
||||||
color: Some(run.color),
|
|
||||||
thickness: px(1.0),
|
|
||||||
wavy: false,
|
|
||||||
}),
|
|
||||||
..run.clone()
|
..run.clone()
|
||||||
},
|
},
|
||||||
TextRun {
|
TextRun {
|
||||||
@@ -449,9 +524,8 @@ impl Element for TextElement {
|
|||||||
vec![run]
|
vec![run]
|
||||||
};
|
};
|
||||||
|
|
||||||
let font_size = style.font_size.to_pixels(window.rem_size());
|
|
||||||
let wrap_width = if multi_line {
|
let wrap_width = if multi_line {
|
||||||
Some(bounds.size.width - RIGHT_MARGIN)
|
Some(bounds.size.width - line_number_width - RIGHT_MARGIN)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
@@ -459,7 +533,25 @@ impl Element for TextElement {
|
|||||||
let lines = window
|
let lines = window
|
||||||
.text_system()
|
.text_system()
|
||||||
.shape_text(display_text, font_size, &runs, wrap_width, None)
|
.shape_text(display_text, font_size, &runs, wrap_width, None)
|
||||||
.unwrap();
|
.expect("failed to shape text");
|
||||||
|
|
||||||
|
let total_wrapped_lines = lines
|
||||||
|
.iter()
|
||||||
|
.map(|line| {
|
||||||
|
// +1 is the first line, `wrap_boundaries` is the wrapped lines after the `\n`.
|
||||||
|
1 + line.wrap_boundaries.len()
|
||||||
|
})
|
||||||
|
.sum::<usize>();
|
||||||
|
|
||||||
|
let max_line_width = lines
|
||||||
|
.iter()
|
||||||
|
.map(|line| line.width())
|
||||||
|
.max()
|
||||||
|
.unwrap_or(bounds.size.width);
|
||||||
|
let scroll_size = size(
|
||||||
|
max_line_width + line_number_width + RIGHT_MARGIN,
|
||||||
|
(total_wrapped_lines as f32 * line_height).max(bounds.size.height),
|
||||||
|
);
|
||||||
|
|
||||||
// `position_for_index` for example
|
// `position_for_index` for example
|
||||||
//
|
//
|
||||||
@@ -492,15 +584,35 @@ 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, cursor_scroll_offset) =
|
let (cursor_bounds, cursor_scroll_offset, _) = self.layout_cursor(
|
||||||
self.layout_cursor(&lines, line_height, &mut bounds, window, cx);
|
&lines,
|
||||||
|
line_height,
|
||||||
|
&mut bounds,
|
||||||
|
line_number_width,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
let selection_path = self.layout_selections(&lines, line_height, &mut bounds, window, cx);
|
let selection_path = self.layout_selections(
|
||||||
|
&lines,
|
||||||
|
line_height,
|
||||||
|
&mut bounds,
|
||||||
|
line_number_width,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
PrepaintState {
|
PrepaintState {
|
||||||
bounds,
|
bounds,
|
||||||
lines,
|
last_layout: LastLayout {
|
||||||
cursor,
|
lines: Rc::new(lines),
|
||||||
|
line_height,
|
||||||
|
visible_range,
|
||||||
|
},
|
||||||
|
scroll_size,
|
||||||
|
line_numbers: None,
|
||||||
|
line_number_width,
|
||||||
|
cursor_bounds,
|
||||||
cursor_scroll_offset,
|
cursor_scroll_offset,
|
||||||
selection_path,
|
selection_path,
|
||||||
}
|
}
|
||||||
@@ -520,6 +632,7 @@ impl Element for TextElement {
|
|||||||
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.input.read(cx).selected_range.clone();
|
||||||
|
let visible_range = &prepaint.last_layout.visible_range;
|
||||||
|
|
||||||
window.handle_input(
|
window.handle_input(
|
||||||
&focus_handle,
|
&focus_handle,
|
||||||
@@ -551,59 +664,76 @@ impl Element for TextElement {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Paint multi line text
|
||||||
|
let line_height = window.line_height();
|
||||||
|
let origin = bounds.origin;
|
||||||
|
|
||||||
|
let mut invisible_top_padding = px(0.);
|
||||||
|
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.);
|
||||||
|
if self.input.read(cx).masked {
|
||||||
|
// Move down offset for vertical centering the *****
|
||||||
|
if cfg!(target_os = "macos") {
|
||||||
|
mask_offset_y = px(3.);
|
||||||
|
} else {
|
||||||
|
mask_offset_y = px(2.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut offset_y = px(0.);
|
||||||
|
if let Some(line_numbers) = prepaint.line_numbers.as_ref() {
|
||||||
|
offset_y += invisible_top_padding;
|
||||||
|
|
||||||
|
// Each item is the normal lines.
|
||||||
|
for lines in line_numbers.iter() {
|
||||||
|
for line in lines {
|
||||||
|
let p = point(origin.x, origin.y + offset_y);
|
||||||
|
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 let Some(path) = prepaint.selection_path.take() {
|
if let Some(path) = prepaint.selection_path.take() {
|
||||||
window.paint_path(path, cx.theme().element_disabled);
|
window.paint_path(path, cx.theme().element_disabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Paint multi line text
|
// Paint text
|
||||||
let line_height = window.line_height();
|
let mut offset_y = mask_offset_y + invisible_top_padding;
|
||||||
let origin = bounds.origin;
|
for line in prepaint
|
||||||
|
.last_layout
|
||||||
let mut offset_y = px(0.);
|
.iter()
|
||||||
if self.input.read(cx).masked {
|
.skip(visible_range.start)
|
||||||
// Move down offset for vertical centering the *****
|
.take(visible_range.len())
|
||||||
if cfg!(target_os = "macos") {
|
{
|
||||||
offset_y = px(3.);
|
let p = point(origin.x + prepaint.line_number_width, origin.y + offset_y);
|
||||||
} else {
|
|
||||||
offset_y = px(2.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for line in prepaint.lines.iter() {
|
|
||||||
let p = point(origin.x, 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 {
|
if focused {
|
||||||
if let Some(cursor) = prepaint.cursor.take() {
|
if let Some(mut cursor_bounds) = prepaint.cursor_bounds.take() {
|
||||||
window.paint_quad(cursor);
|
cursor_bounds.origin.y += prepaint.cursor_scroll_offset.y;
|
||||||
|
window.paint_quad(fill(cursor_bounds, cx.theme().cursor));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let width = prepaint
|
|
||||||
.lines
|
|
||||||
.iter()
|
|
||||||
.map(|l| l.width())
|
|
||||||
.max()
|
|
||||||
.unwrap_or_default();
|
|
||||||
let height = offset_y;
|
|
||||||
let scroll_size = size(width, height);
|
|
||||||
|
|
||||||
self.input.update(cx, |input, cx| {
|
self.input.update(cx, |input, cx| {
|
||||||
input.last_layout = Some(prepaint.lines.clone());
|
input.last_layout = Some(prepaint.last_layout.clone());
|
||||||
input.last_bounds = Some(bounds);
|
input.last_bounds = Some(bounds);
|
||||||
input.last_cursor_offset = Some(input.cursor_offset());
|
input.last_cursor_offset = Some(input.cursor_offset());
|
||||||
input.last_line_height = line_height;
|
|
||||||
|
|
||||||
input.set_input_bounds(input_bounds, cx);
|
input.set_input_bounds(input_bounds, cx);
|
||||||
|
|
||||||
input.last_selected_range = Some(selected_range);
|
input.last_selected_range = Some(selected_range);
|
||||||
input.scroll_size = scroll_size;
|
input.scroll_size = prepaint.scroll_size;
|
||||||
|
input.line_number_width = prepaint.line_number_width;
|
||||||
input
|
input
|
||||||
.scroll_handle
|
.scroll_handle
|
||||||
.set_offset(prepaint.cursor_scroll_offset);
|
.set_offset(prepaint.cursor_scroll_offset);
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ 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]`
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
use std::ops::Range;
|
use std::ops::{Deref, Range};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
@@ -308,6 +309,24 @@ impl InputMode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(super) struct LastLayout {
|
||||||
|
/// The last layout lines.
|
||||||
|
pub(super) lines: Rc<SmallVec<[WrappedLine; 1]>>,
|
||||||
|
/// The line_height of text layout, this will change will InputElement painted.
|
||||||
|
pub(super) line_height: Pixels,
|
||||||
|
/// The visible range (no wrap) of lines in the viewport.
|
||||||
|
pub(super) visible_range: Range<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for LastLayout {
|
||||||
|
type Target = Rc<SmallVec<[WrappedLine; 1]>>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.lines
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// InputState to keep editing state of the [`super::TextInput`].
|
/// InputState to keep editing state of the [`super::TextInput`].
|
||||||
pub struct InputState {
|
pub struct InputState {
|
||||||
pub(super) focus_handle: FocusHandle,
|
pub(super) focus_handle: FocusHandle,
|
||||||
@@ -322,11 +341,10 @@ pub struct InputState {
|
|||||||
/// Range for save the selected word, use to keep word range when drag move.
|
/// Range for save the selected word, use to keep word range when drag move.
|
||||||
pub(super) selected_word_range: Option<Range<usize>>,
|
pub(super) selected_word_range: Option<Range<usize>>,
|
||||||
pub(super) selection_reversed: bool,
|
pub(super) selection_reversed: bool,
|
||||||
|
/// The marked range is the temporary insert text on IME typing.
|
||||||
pub(super) marked_range: Option<Range<usize>>,
|
pub(super) marked_range: Option<Range<usize>>,
|
||||||
pub(super) last_layout: Option<SmallVec<[WrappedLine; 1]>>,
|
pub(super) last_layout: Option<LastLayout>,
|
||||||
pub(super) last_cursor_offset: Option<usize>,
|
pub(super) last_cursor_offset: Option<usize>,
|
||||||
/// The line_height of text layout, this will change will InputElement painted.
|
|
||||||
pub(super) last_line_height: Pixels,
|
|
||||||
/// The input container bounds
|
/// The input container bounds
|
||||||
pub(super) input_bounds: Bounds<Pixels>,
|
pub(super) input_bounds: Bounds<Pixels>,
|
||||||
/// The text bounds
|
/// The text bounds
|
||||||
@@ -343,12 +361,13 @@ pub struct InputState {
|
|||||||
pub(super) scrollbar_state: Rc<Cell<ScrollbarState>>,
|
pub(super) scrollbar_state: Rc<Cell<ScrollbarState>>,
|
||||||
/// The size of the scrollable content.
|
/// The size of the scrollable content.
|
||||||
pub(crate) scroll_size: gpui::Size<Pixels>,
|
pub(crate) scroll_size: gpui::Size<Pixels>,
|
||||||
|
pub(crate) line_number_width: Pixels,
|
||||||
|
|
||||||
/// The mask pattern for formatting the input text
|
/// The mask pattern for formatting the input text
|
||||||
pub(crate) mask_pattern: MaskPattern,
|
pub(crate) mask_pattern: MaskPattern,
|
||||||
pub(super) placeholder: SharedString,
|
pub(super) placeholder: SharedString,
|
||||||
|
|
||||||
/// To remember the horizontal column (x-coordinate) of the cursor position.
|
/// To remember the horizontal column (x-coordinate) of the cursor position for keep column for move up/down.
|
||||||
preferred_x_offset: Option<Pixels>,
|
preferred_x_offset: Option<Pixels>,
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
}
|
}
|
||||||
@@ -362,7 +381,9 @@ impl InputState {
|
|||||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let focus_handle = cx.focus_handle();
|
let focus_handle = cx.focus_handle();
|
||||||
let blink_cursor = cx.new(|_| BlinkCursor::new());
|
let blink_cursor = cx.new(|_| BlinkCursor::new());
|
||||||
let history = History::new().group_interval(std::time::Duration::from_secs(1));
|
let history = History::new()
|
||||||
|
.max_undo(2000)
|
||||||
|
.group_interval(Duration::from_millis(600));
|
||||||
|
|
||||||
let _subscriptions = vec![
|
let _subscriptions = vec![
|
||||||
// Observe the blink cursor to repaint the view when it changes.
|
// Observe the blink cursor to repaint the view when it changes.
|
||||||
@@ -411,11 +432,11 @@ impl InputState {
|
|||||||
last_layout: None,
|
last_layout: None,
|
||||||
last_bounds: None,
|
last_bounds: None,
|
||||||
last_selected_range: None,
|
last_selected_range: None,
|
||||||
last_line_height: px(19.),
|
|
||||||
last_cursor_offset: None,
|
last_cursor_offset: None,
|
||||||
scroll_handle: ScrollHandle::new(),
|
scroll_handle: ScrollHandle::new(),
|
||||||
scrollbar_state: Rc::new(Cell::new(ScrollbarState::default())),
|
scrollbar_state: Rc::new(Cell::new(ScrollbarState::default())),
|
||||||
scroll_size: gpui::size(px(0.), px(0.)),
|
scroll_size: gpui::size(px(0.), px(0.)),
|
||||||
|
line_number_width: px(0.),
|
||||||
preferred_x_offset: None,
|
preferred_x_offset: None,
|
||||||
placeholder: SharedString::default(),
|
placeholder: SharedString::default(),
|
||||||
mask_pattern: MaskPattern::default(),
|
mask_pattern: MaskPattern::default(),
|
||||||
@@ -469,32 +490,37 @@ impl InputState {
|
|||||||
|
|
||||||
/// Called after moving the cursor. Updates preferred_x_offset if we know where the cursor now is.
|
/// Called after moving the cursor. Updates preferred_x_offset if we know where the cursor now is.
|
||||||
fn update_preferred_x_offset(&mut self, _cx: &mut Context<Self>) {
|
fn update_preferred_x_offset(&mut self, _cx: &mut Context<Self>) {
|
||||||
if let (Some(lines), Some(bounds)) = (&self.last_layout, &self.last_bounds) {
|
let (Some(_), Some(bounds)) = (&self.last_layout, &self.last_bounds) else {
|
||||||
let offset = self.cursor_offset();
|
return;
|
||||||
let line_height = self.last_line_height;
|
};
|
||||||
|
|
||||||
// Find which line and sub-line the cursor is on and its position
|
// Find which line and sub-line the cursor is on and its position
|
||||||
let (_line_index, _sub_line_index, cursor_pos) =
|
let (_, _, cursor_pos) = self.line_and_position_for_offset(self.cursor_offset());
|
||||||
self.line_and_position_for_offset(offset, lines, line_height);
|
|
||||||
|
|
||||||
if let Some(pos) = cursor_pos {
|
if let Some(pos) = cursor_pos {
|
||||||
// Adjust by scroll offset
|
self.preferred_x_offset = Some(pos.x + bounds.origin.x);
|
||||||
let scroll_offset = bounds.origin;
|
|
||||||
self.preferred_x_offset = Some(pos.x + scroll_offset.x);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find which line and sub-line the given offset belongs to, along with the position within that sub-line.
|
/// Find which line and sub-line the given offset belongs to, along with the position within that sub-line.
|
||||||
fn line_and_position_for_offset(
|
///
|
||||||
|
/// Returns:
|
||||||
|
///
|
||||||
|
/// - The index of the line (zero-based) containing the offset.
|
||||||
|
/// - The index of the sub-line (zero-based) within the line containing the offset.
|
||||||
|
/// - The position of the offset.
|
||||||
|
pub(super) fn line_and_position_for_offset(
|
||||||
&self,
|
&self,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
lines: &[WrappedLine],
|
|
||||||
line_height: Pixels,
|
|
||||||
) -> (usize, usize, Option<Point<Pixels>>) {
|
) -> (usize, usize, Option<Point<Pixels>>) {
|
||||||
|
let Some(last_layout) = &self.last_layout else {
|
||||||
|
return (0, 0, None);
|
||||||
|
};
|
||||||
|
let line_height = last_layout.line_height;
|
||||||
|
|
||||||
let mut prev_lines_offset = 0;
|
let mut prev_lines_offset = 0;
|
||||||
let mut y_offset = px(0.);
|
let mut y_offset = px(0.);
|
||||||
for (line_index, line) in lines.iter().enumerate() {
|
for (line_index, line) in last_layout.lines.iter().enumerate() {
|
||||||
let local_offset = offset.saturating_sub(prev_lines_offset);
|
let local_offset = offset.saturating_sub(prev_lines_offset);
|
||||||
if let Some(pos) = line.position_for_index(local_offset, line_height) {
|
if let Some(pos) = line.position_for_index(local_offset, line_height) {
|
||||||
let sub_line_index = (pos.y.0 / line_height.0) as usize;
|
let sub_line_index = (pos.y.0 / line_height.0) as usize;
|
||||||
@@ -515,14 +541,15 @@ impl InputState {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let (Some(lines), Some(bounds)) = (&self.last_layout, &self.last_bounds) else {
|
let (Some(last_layout), Some(bounds)) = (&self.last_layout, &self.last_bounds) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let offset = self.cursor_offset();
|
let offset = self.cursor_offset();
|
||||||
let line_height = self.last_line_height;
|
let preferred_x_offset = self.preferred_x_offset;
|
||||||
|
let line_height = last_layout.line_height;
|
||||||
let (current_line_index, current_sub_line, current_pos) =
|
let (current_line_index, current_sub_line, current_pos) =
|
||||||
self.line_and_position_for_offset(offset, lines, line_height);
|
self.line_and_position_for_offset(offset);
|
||||||
|
|
||||||
let Some(current_pos) = current_pos else {
|
let Some(current_pos) = current_pos else {
|
||||||
return;
|
return;
|
||||||
@@ -544,24 +571,17 @@ impl InputState {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle moving below the last line
|
|
||||||
if direction == 1 && new_line_index == 0 && new_sub_line > 0 && lines.len() == 1 {
|
|
||||||
// Move cursor to the end of the text
|
|
||||||
self.move_to(self.text.len(), window, cx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if new_sub_line < 0 {
|
if new_sub_line < 0 {
|
||||||
if new_line_index > 0 {
|
if new_line_index > 0 {
|
||||||
new_line_index -= 1;
|
new_line_index -= 1;
|
||||||
new_sub_line = lines[new_line_index].wrap_boundaries.len() as i32;
|
new_sub_line = last_layout.lines[new_line_index].wrap_boundaries.len() as i32;
|
||||||
} else {
|
} else {
|
||||||
new_sub_line = 0;
|
new_sub_line = 0;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let max_sub_line = lines[new_line_index].wrap_boundaries.len() as i32;
|
let max_sub_line = last_layout.lines[new_line_index].wrap_boundaries.len() as i32;
|
||||||
if new_sub_line > max_sub_line {
|
if new_sub_line > max_sub_line {
|
||||||
if new_line_index < lines.len() - 1 {
|
if new_line_index < last_layout.lines.len() - 1 {
|
||||||
new_line_index += 1;
|
new_line_index += 1;
|
||||||
new_sub_line = 0;
|
new_sub_line = 0;
|
||||||
} else {
|
} else {
|
||||||
@@ -575,7 +595,7 @@ impl InputState {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let target_line = &lines[new_line_index];
|
let target_line = &last_layout.lines[new_line_index];
|
||||||
let line_x = current_x - bounds.origin.x;
|
let line_x = current_x - bounds.origin.x;
|
||||||
let target_sub_line = new_sub_line as usize;
|
let target_sub_line = new_sub_line as usize;
|
||||||
|
|
||||||
@@ -583,12 +603,12 @@ impl InputState {
|
|||||||
let index_res = target_line.index_for_position(approx_pos, line_height);
|
let index_res = target_line.index_for_position(approx_pos, line_height);
|
||||||
|
|
||||||
let new_local_index = match index_res {
|
let new_local_index = match index_res {
|
||||||
Ok(i) => i + 1,
|
Ok(i) => i,
|
||||||
Err(i) => i,
|
Err(i) => i,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut prev_lines_offset = 0;
|
let mut prev_lines_offset = 0;
|
||||||
for (i, l) in lines.iter().enumerate() {
|
for (i, l) in last_layout.lines.iter().enumerate() {
|
||||||
if i == new_line_index {
|
if i == new_line_index {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -598,6 +618,8 @@ impl InputState {
|
|||||||
let new_offset = (prev_lines_offset + new_local_index).min(self.text.len());
|
let new_offset = (prev_lines_offset + new_local_index).min(self.text.len());
|
||||||
self.selected_range = new_offset..new_offset;
|
self.selected_range = new_offset..new_offset;
|
||||||
self.pause_blink_cursor(cx);
|
self.pause_blink_cursor(cx);
|
||||||
|
// Set back the preferred_x_offset
|
||||||
|
self.preferred_x_offset = preferred_x_offset;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -669,8 +691,8 @@ impl InputState {
|
|||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let text: SharedString = text.into();
|
let text: SharedString = text.into();
|
||||||
let range = self.range_to_utf16(&(self.cursor_offset()..self.cursor_offset()));
|
let range_utf16 = self.range_to_utf16(&(self.cursor_offset()..self.cursor_offset()));
|
||||||
self.replace_text_in_range(Some(range), &text, window, cx);
|
self.replace_text_in_range(Some(range_utf16), &text, window, cx);
|
||||||
self.selected_range = self.selected_range.end..self.selected_range.end;
|
self.selected_range = self.selected_range.end..self.selected_range.end;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -699,12 +721,27 @@ impl InputState {
|
|||||||
self.replace_text_in_range(Some(range), &text, window, cx);
|
self.replace_text_in_range(Some(range), &text, window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set with disabled mode.
|
||||||
|
///
|
||||||
|
/// See also: [`Self::set_disabled`], [`Self::is_disabled`].
|
||||||
|
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||||
|
self.disabled = disabled;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the disabled state of the input field.
|
/// Set the disabled state of the input field.
|
||||||
|
///
|
||||||
|
/// See also: [`Self::disabled`], [`Self::is_disabled`].
|
||||||
pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
|
pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
|
||||||
self.disabled = disabled;
|
self.disabled = disabled;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return is the input field is disabled.
|
||||||
|
pub fn is_disabled(&self) -> bool {
|
||||||
|
self.disabled
|
||||||
|
}
|
||||||
|
|
||||||
/// Set with password masked state.
|
/// Set with password masked state.
|
||||||
pub fn masked(mut self, masked: bool) -> Self {
|
pub fn masked(mut self, masked: bool) -> Self {
|
||||||
self.masked = masked;
|
self.masked = masked;
|
||||||
@@ -767,10 +804,6 @@ impl InputState {
|
|||||||
self.mask_pattern.unmask(&self.text).into()
|
self.mask_pattern.unmask(&self.text).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn disabled(&self) -> bool {
|
|
||||||
self.disabled
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Focus the input field.
|
/// Focus the input field.
|
||||||
pub fn focus(&self, window: &mut Window, _: &mut Context<Self>) {
|
pub fn focus(&self, window: &mut Window, _: &mut Context<Self>) {
|
||||||
self.focus_handle.focus(window);
|
self.focus_handle.focus(window);
|
||||||
@@ -798,6 +831,13 @@ impl InputState {
|
|||||||
if self.is_single_line() {
|
if self.is_single_line() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if !self.selected_range.is_empty() {
|
||||||
|
self.move_to(
|
||||||
|
self.previous_boundary(self.selected_range.start.saturating_sub(1)),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
self.pause_blink_cursor(cx);
|
self.pause_blink_cursor(cx);
|
||||||
self.move_vertical(-1, window, cx);
|
self.move_vertical(-1, window, cx);
|
||||||
}
|
}
|
||||||
@@ -806,6 +846,13 @@ impl InputState {
|
|||||||
if self.is_single_line() {
|
if self.is_single_line() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if !self.selected_range.is_empty() {
|
||||||
|
self.move_to(
|
||||||
|
self.next_boundary(self.selected_range.end.saturating_sub(1)),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
self.pause_blink_cursor(cx);
|
self.pause_blink_cursor(cx);
|
||||||
self.move_vertical(1, window, cx);
|
self.move_vertical(1, window, cx);
|
||||||
}
|
}
|
||||||
@@ -833,7 +880,7 @@ impl InputState {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let offset = self.start_of_line(window, cx).saturating_sub(1);
|
let offset = self.start_of_line(window, cx).saturating_sub(1);
|
||||||
self.select_to(offset, window, cx);
|
self.select_to(self.previous_boundary(offset), window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn select_down(
|
pub(super) fn select_down(
|
||||||
@@ -882,11 +929,11 @@ impl InputState {
|
|||||||
self.replace_text_in_range(None, "\n", window, cx);
|
self.replace_text_in_range(None, "\n", window, cx);
|
||||||
|
|
||||||
// Move cursor to the start of the next line
|
// Move cursor to the start of the next line
|
||||||
let mut new_offset = self.next_boundary(self.cursor_offset()) - 1;
|
let mut new_offset = self.cursor_offset() - 1;
|
||||||
if is_eof {
|
if is_eof {
|
||||||
new_offset += 1;
|
new_offset += 1;
|
||||||
}
|
}
|
||||||
self.move_to(new_offset, window, cx);
|
self.move_to(self.next_boundary(new_offset), window, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -991,8 +1038,8 @@ impl InputState {
|
|||||||
/// Return the start offset of the previous word.
|
/// Return the start offset of the previous word.
|
||||||
fn previous_start_of_word(&mut self) -> usize {
|
fn previous_start_of_word(&mut self) -> usize {
|
||||||
let offset = self.selected_range.start;
|
let offset = self.selected_range.start;
|
||||||
let prev_str = &self.text[..offset].to_string();
|
let prev_str = self.text_for_range_utf8(0..offset);
|
||||||
UnicodeSegmentation::split_word_bound_indices(prev_str as &str)
|
UnicodeSegmentation::split_word_bound_indices(prev_str)
|
||||||
.filter(|(_, s)| !s.trim_start().is_empty())
|
.filter(|(_, s)| !s.trim_start().is_empty())
|
||||||
.next_back()
|
.next_back()
|
||||||
.map(|(i, _)| i)
|
.map(|(i, _)| i)
|
||||||
@@ -1002,8 +1049,8 @@ impl InputState {
|
|||||||
/// Return the next end offset of the next word.
|
/// Return the next end offset of the next word.
|
||||||
fn next_end_of_word(&mut self) -> usize {
|
fn next_end_of_word(&mut self) -> usize {
|
||||||
let offset = self.cursor_offset();
|
let offset = self.cursor_offset();
|
||||||
let next_str = &self.text[offset..].to_string();
|
let next_str = self.text_for_range_utf8(offset..self.text.len());
|
||||||
UnicodeSegmentation::split_word_bound_indices(next_str as &str)
|
UnicodeSegmentation::split_word_bound_indices(next_str)
|
||||||
.find(|(_, s)| !s.trim_start().is_empty())
|
.find(|(_, s)| !s.trim_start().is_empty())
|
||||||
.map(|(i, s)| offset + i + s.len())
|
.map(|(i, s)| offset + i + s.len())
|
||||||
.unwrap_or(self.text.len())
|
.unwrap_or(self.text.len())
|
||||||
@@ -1034,7 +1081,7 @@ impl InputState {
|
|||||||
// ignore if offset is "\n"
|
// ignore if offset is "\n"
|
||||||
if self
|
if self
|
||||||
.text_for_range(
|
.text_for_range(
|
||||||
self.range_to_utf16(&(offset - 1..offset)),
|
self.range_to_utf16(&(offset.saturating_sub(1)..offset)),
|
||||||
&mut None,
|
&mut None,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
@@ -1150,11 +1197,11 @@ impl InputState {
|
|||||||
self.replace_text_in_range(None, "\n", window, cx);
|
self.replace_text_in_range(None, "\n", window, cx);
|
||||||
|
|
||||||
// Move cursor to the start of the next line
|
// Move cursor to the start of the next line
|
||||||
let mut new_offset = self.next_boundary(self.cursor_offset()) - 1;
|
let mut new_offset = self.cursor_offset() - 1;
|
||||||
if is_eof {
|
if is_eof {
|
||||||
new_offset += 1;
|
new_offset += 1;
|
||||||
}
|
}
|
||||||
self.move_to(new_offset, window, cx);
|
self.move_to(self.next_boundary(new_offset), window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.emit(InputEvent::PressEnter {
|
cx.emit(InputEvent::PressEnter {
|
||||||
@@ -1167,6 +1214,10 @@ impl InputState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn escape(&mut self, _: &Escape, window: &mut Window, cx: &mut Context<Self>) {
|
pub(super) fn escape(&mut self, _: &Escape, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.marked_range.is_some() {
|
||||||
|
self.unmark_text(window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
if !self.selected_range.is_empty() {
|
if !self.selected_range.is_empty() {
|
||||||
return self.unselect(window, cx);
|
return self.unselect(window, cx);
|
||||||
}
|
}
|
||||||
@@ -1192,6 +1243,14 @@ impl InputState {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If there have IME marked range and is empty (Means pressed Esc to abort IME typing)
|
||||||
|
// Clear the marked range.
|
||||||
|
if let Some(marked_range) = &self.marked_range {
|
||||||
|
if marked_range.is_empty() {
|
||||||
|
self.marked_range = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.selecting = true;
|
self.selecting = true;
|
||||||
let offset = self.index_for_mouse_position(event.position, window, cx);
|
let offset = self.index_for_mouse_position(event.position, window, cx);
|
||||||
|
|
||||||
@@ -1221,10 +1280,15 @@ impl InputState {
|
|||||||
pub(super) fn on_scroll_wheel(
|
pub(super) fn on_scroll_wheel(
|
||||||
&mut self,
|
&mut self,
|
||||||
event: &ScrollWheelEvent,
|
event: &ScrollWheelEvent,
|
||||||
_window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let delta = event.delta.pixel_delta(self.last_line_height);
|
let line_height = self
|
||||||
|
.last_layout
|
||||||
|
.as_ref()
|
||||||
|
.map(|layout| layout.line_height)
|
||||||
|
.unwrap_or(window.line_height());
|
||||||
|
let delta = event.delta.pixel_delta(line_height);
|
||||||
self.update_scroll_offset(Some(self.scroll_handle.offset() + delta), cx);
|
self.update_scroll_offset(Some(self.scroll_handle.offset() + delta), cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1256,7 +1320,9 @@ impl InputState {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let selected_text = self.text[self.selected_range.clone()].to_string();
|
let selected_text = self
|
||||||
|
.text_for_range_utf8(self.selected_range.clone())
|
||||||
|
.to_string();
|
||||||
cx.write_to_clipboard(ClipboardItem::new_string(selected_text));
|
cx.write_to_clipboard(ClipboardItem::new_string(selected_text));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1265,7 +1331,9 @@ impl InputState {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let selected_text = self.text[self.selected_range.clone()].to_string();
|
let selected_text = self
|
||||||
|
.text_for_range_utf8(self.selected_range.clone())
|
||||||
|
.to_string();
|
||||||
cx.write_to_clipboard(ClipboardItem::new_string(selected_text));
|
cx.write_to_clipboard(ClipboardItem::new_string(selected_text));
|
||||||
self.replace_text_in_range(None, "", window, cx);
|
self.replace_text_in_range(None, "", window, cx);
|
||||||
}
|
}
|
||||||
@@ -1356,6 +1424,10 @@ impl InputState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn cursor_offset(&self) -> usize {
|
pub(super) fn cursor_offset(&self) -> usize {
|
||||||
|
if let Some(marked_range) = &self.marked_range {
|
||||||
|
return marked_range.end;
|
||||||
|
}
|
||||||
|
|
||||||
if self.selection_reversed {
|
if self.selection_reversed {
|
||||||
self.selected_range.start
|
self.selected_range.start
|
||||||
} else {
|
} else {
|
||||||
@@ -1374,12 +1446,13 @@ impl InputState {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let (Some(bounds), Some(lines)) = (self.last_bounds.as_ref(), self.last_layout.as_ref())
|
let (Some(bounds), Some(last_layout)) =
|
||||||
|
(self.last_bounds.as_ref(), self.last_layout.as_ref())
|
||||||
else {
|
else {
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
let line_height = self.last_line_height;
|
let line_height = last_layout.line_height;
|
||||||
|
|
||||||
// TIP: About the IBeam cursor
|
// TIP: About the IBeam cursor
|
||||||
//
|
//
|
||||||
@@ -1396,7 +1469,7 @@ impl InputState {
|
|||||||
let mut index = 0;
|
let mut index = 0;
|
||||||
let mut y_offset = px(0.);
|
let mut y_offset = px(0.);
|
||||||
|
|
||||||
for line in lines.iter() {
|
for line in last_layout.iter() {
|
||||||
let line_origin = self.line_origin_with_y_offset(&mut y_offset, line, line_height);
|
let line_origin = self.line_origin_with_y_offset(&mut y_offset, line, line_height);
|
||||||
let pos = inner_position - line_origin;
|
let pos = inner_position - line_origin;
|
||||||
|
|
||||||
@@ -1508,21 +1581,29 @@ impl InputState {
|
|||||||
let mut start = self.offset_to_utf16(offset);
|
let mut start = self.offset_to_utf16(offset);
|
||||||
let mut end = start;
|
let mut end = start;
|
||||||
let prev_text = self
|
let prev_text = self
|
||||||
.text_for_range(0..start, &mut None, window, cx)
|
.text_for_range(self.range_to_utf16(&(0..start)), &mut None, window, cx)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let next_text = self
|
let next_text = self
|
||||||
.text_for_range(end..self.text.len(), &mut None, window, cx)
|
.text_for_range(
|
||||||
|
self.range_to_utf16(&(end..self.text.len())),
|
||||||
|
&mut None,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let prev_chars = prev_text.chars().rev().peekable();
|
let prev_chars = prev_text.chars().rev();
|
||||||
let next_chars = next_text.chars().peekable();
|
let next_chars = next_text.chars();
|
||||||
|
let pre_chars_count = prev_chars.clone().count();
|
||||||
|
|
||||||
for c in prev_chars {
|
for (ix, c) in prev_chars.enumerate() {
|
||||||
if !is_word(c) {
|
if !is_word(c) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
start -= c.len_utf16();
|
if ix < pre_chars_count {
|
||||||
|
start = start.saturating_sub(c.len_utf8());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for c in next_chars {
|
for c in next_chars {
|
||||||
@@ -1530,10 +1611,14 @@ impl InputState {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
end += c.len_utf16();
|
end += c.len_utf8();
|
||||||
}
|
}
|
||||||
|
|
||||||
self.selected_range = self.range_from_utf16(&(start..end));
|
if start == end {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.selected_range = start..end;
|
||||||
self.selected_word_range = Some(self.selected_range.clone());
|
self.selected_word_range = Some(self.selected_range.clone());
|
||||||
cx.notify()
|
cx.notify()
|
||||||
}
|
}
|
||||||
@@ -1599,7 +1684,9 @@ impl InputState {
|
|||||||
|
|
||||||
/// Returns the true to let InputElement to render cursor, when Input is focused and current BlinkCursor is visible.
|
/// Returns the true to let InputElement to render cursor, when Input is focused and current BlinkCursor is visible.
|
||||||
pub(crate) fn show_cursor(&self, window: &Window, cx: &App) -> bool {
|
pub(crate) fn show_cursor(&self, window: &Window, cx: &App) -> bool {
|
||||||
self.focus_handle.is_focused(window) && self.blink_cursor.read(cx).visible()
|
self.focus_handle.is_focused(window)
|
||||||
|
&& self.blink_cursor.read(cx).visible()
|
||||||
|
&& window.is_window_active()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_focus(&mut self, _: &mut Window, cx: &mut Context<Self>) {
|
fn on_focus(&mut self, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
@@ -1718,6 +1805,11 @@ impl InputState {
|
|||||||
self.mode.update_auto_grow(&self.text_wrapper);
|
self.mode.update_auto_grow(&self.text_wrapper);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn text_for_range_utf8(&mut self, range: impl Into<Range<usize>>) -> &str {
|
||||||
|
let range = self.range_from_utf16(&self.range_to_utf16(&range.into()));
|
||||||
|
&self.text[range]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EntityInputHandler for InputState {
|
impl EntityInputHandler for InputState {
|
||||||
@@ -1780,8 +1872,10 @@ impl EntityInputHandler for InputState {
|
|||||||
.or(self.marked_range.clone())
|
.or(self.marked_range.clone())
|
||||||
.unwrap_or(self.selected_range.clone());
|
.unwrap_or(self.selected_range.clone());
|
||||||
|
|
||||||
let pending_text: SharedString =
|
let pending_text: SharedString = (self.text_for_range_utf8(0..range.start).to_owned()
|
||||||
(self.text[0..range.start].to_owned() + new_text + &self.text[range.end..]).into();
|
+ new_text
|
||||||
|
+ self.text_for_range_utf8(range.end..self.text.len()))
|
||||||
|
.into();
|
||||||
|
|
||||||
// Check if the new text is valid
|
// Check if the new text is valid
|
||||||
if !self.is_valid_input(&pending_text) {
|
if !self.is_valid_input(&pending_text) {
|
||||||
@@ -1794,7 +1888,7 @@ impl EntityInputHandler for InputState {
|
|||||||
|
|
||||||
self.push_history(&range, new_text, window, cx);
|
self.push_history(&range, new_text, window, cx);
|
||||||
self.text = mask_text;
|
self.text = mask_text;
|
||||||
self.text_wrapper.update(self.text.clone(), cx);
|
self.text_wrapper.update(&self.text, false, cx);
|
||||||
self.selected_range = new_pos..new_pos;
|
self.selected_range = new_pos..new_pos;
|
||||||
self.marked_range.take();
|
self.marked_range.take();
|
||||||
self.update_preferred_x_offset(cx);
|
self.update_preferred_x_offset(cx);
|
||||||
@@ -1804,6 +1898,7 @@ impl EntityInputHandler for InputState {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Mark text is the IME temporary insert on typing.
|
||||||
fn replace_and_mark_text_in_range(
|
fn replace_and_mark_text_in_range(
|
||||||
&mut self,
|
&mut self,
|
||||||
range_utf16: Option<Range<usize>>,
|
range_utf16: Option<Range<usize>>,
|
||||||
@@ -1821,20 +1916,32 @@ impl EntityInputHandler for InputState {
|
|||||||
.map(|range_utf16| self.range_from_utf16(range_utf16))
|
.map(|range_utf16| self.range_from_utf16(range_utf16))
|
||||||
.or(self.marked_range.clone())
|
.or(self.marked_range.clone())
|
||||||
.unwrap_or(self.selected_range.clone());
|
.unwrap_or(self.selected_range.clone());
|
||||||
let pending_text: SharedString =
|
|
||||||
(self.text[0..range.start].to_owned() + new_text + &self.text[range.end..]).into();
|
let pending_text: SharedString = (self.text_for_range_utf8(0..range.start).to_owned()
|
||||||
|
+ new_text
|
||||||
|
+ self.text_for_range_utf8(range.end..self.text.len()))
|
||||||
|
.into();
|
||||||
|
|
||||||
if !self.is_valid_input(&pending_text) {
|
if !self.is_valid_input(&pending_text) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.push_history(&range, new_text, window, cx);
|
self.push_history(&range, new_text, window, cx);
|
||||||
self.text = pending_text;
|
self.text = pending_text;
|
||||||
|
self.text_wrapper.update(&self.text, false, cx);
|
||||||
|
if new_text.is_empty() {
|
||||||
|
// Cancel selection, when cancel IME input.
|
||||||
|
self.selected_range = range.start..range.start;
|
||||||
|
self.marked_range = None;
|
||||||
|
} else {
|
||||||
self.marked_range = Some(range.start..range.start + new_text.len());
|
self.marked_range = Some(range.start..range.start + new_text.len());
|
||||||
self.selected_range = new_selected_range_utf16
|
self.selected_range = new_selected_range_utf16
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|range_utf16| self.range_from_utf16(range_utf16))
|
.map(|range_utf16| self.range_from_utf16(range_utf16))
|
||||||
.map(|new_range| new_range.start + range.start..new_range.end + range.end)
|
.map(|new_range| new_range.start + range.start..new_range.end + range.end)
|
||||||
.unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len());
|
.unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len());
|
||||||
|
}
|
||||||
|
self.mode.update_auto_grow(&self.text_wrapper);
|
||||||
cx.emit(InputEvent::Change(self.unmask_value()));
|
cx.emit(InputEvent::Change(self.unmask_value()));
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
@@ -1848,8 +1955,8 @@ impl EntityInputHandler for InputState {
|
|||||||
_window: &mut Window,
|
_window: &mut Window,
|
||||||
_cx: &mut Context<Self>,
|
_cx: &mut Context<Self>,
|
||||||
) -> Option<Bounds<Pixels>> {
|
) -> Option<Bounds<Pixels>> {
|
||||||
let line_height = self.last_line_height;
|
let last_layout = self.last_layout.as_ref()?;
|
||||||
let lines = self.last_layout.as_ref()?;
|
let line_height = last_layout.line_height;
|
||||||
let range = self.range_from_utf16(&range_utf16);
|
let range = self.range_from_utf16(&range_utf16);
|
||||||
|
|
||||||
let mut start_origin = None;
|
let mut start_origin = None;
|
||||||
@@ -1857,28 +1964,35 @@ impl EntityInputHandler for InputState {
|
|||||||
let mut y_offset = px(0.);
|
let mut y_offset = px(0.);
|
||||||
let mut index_offset = 0;
|
let mut index_offset = 0;
|
||||||
|
|
||||||
for line in lines.iter() {
|
for line in last_layout.lines.iter() {
|
||||||
|
if start_origin.is_some() && end_origin.is_some() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if start_origin.is_none() {
|
||||||
if let Some(p) =
|
if let Some(p) =
|
||||||
line.position_for_index(range.start.saturating_sub(index_offset), line_height)
|
line.position_for_index(range.start.saturating_sub(index_offset), line_height)
|
||||||
{
|
{
|
||||||
start_origin = Some(p + point(px(0.), y_offset));
|
start_origin = Some(p + point(px(0.), y_offset));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if end_origin.is_none() {
|
||||||
if let Some(p) =
|
if let Some(p) =
|
||||||
line.position_for_index(range.end.saturating_sub(index_offset), line_height)
|
line.position_for_index(range.end.saturating_sub(index_offset), line_height)
|
||||||
{
|
{
|
||||||
end_origin = Some(p + point(px(0.), y_offset));
|
end_origin = Some(p + point(px(0.), y_offset));
|
||||||
}
|
}
|
||||||
|
|
||||||
y_offset += line.size(line_height).height;
|
|
||||||
if start_origin.is_some() && end_origin.is_some() {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
index_offset += line.len();
|
index_offset += line.len() + 1;
|
||||||
|
y_offset += line.size(line_height).height;
|
||||||
}
|
}
|
||||||
|
|
||||||
let start_origin = start_origin.unwrap_or_default();
|
let start_origin = start_origin.unwrap_or_default();
|
||||||
let end_origin = end_origin.unwrap_or_default();
|
let mut end_origin = end_origin.unwrap_or_default();
|
||||||
|
// Ensure at same line.
|
||||||
|
end_origin.y = start_origin.y;
|
||||||
|
|
||||||
Some(Bounds::from_corners(
|
Some(Bounds::from_corners(
|
||||||
bounds.origin + start_origin,
|
bounds.origin + start_origin,
|
||||||
@@ -1893,11 +2007,11 @@ impl EntityInputHandler for InputState {
|
|||||||
_window: &mut Window,
|
_window: &mut Window,
|
||||||
_cx: &mut Context<Self>,
|
_cx: &mut Context<Self>,
|
||||||
) -> Option<usize> {
|
) -> Option<usize> {
|
||||||
let line_height = self.last_line_height;
|
let last_layout = self.last_layout.as_ref()?;
|
||||||
|
let line_height = last_layout.line_height;
|
||||||
let line_point = self.last_bounds?.localize(&point)?;
|
let line_point = self.last_bounds?.localize(&point)?;
|
||||||
let lines = self.last_layout.as_ref()?;
|
|
||||||
|
|
||||||
for line in lines.iter() {
|
for line in last_layout.lines.iter() {
|
||||||
if let Ok(utf8_index) = line.index_for_position(line_point, line_height) {
|
if let Ok(utf8_index) = line.index_for_position(line_point, line_height) {
|
||||||
return Some(self.offset_to_utf16(utf8_index));
|
return Some(self.offset_to_utf16(utf8_index));
|
||||||
}
|
}
|
||||||
@@ -1915,11 +2029,13 @@ impl Focusable for InputState {
|
|||||||
|
|
||||||
impl Render for InputState {
|
impl Render for InputState {
|
||||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
self.text_wrapper.update(&self.text, false, cx);
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.id("text-element")
|
.id("input-state")
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.flex_grow()
|
|
||||||
.when(self.is_multi_line(), |this| this.h_full())
|
.when(self.is_multi_line(), |this| this.h_full())
|
||||||
|
.flex_grow()
|
||||||
.overflow_x_hidden()
|
.overflow_x_hidden()
|
||||||
.child(TextElement::new(cx.entity().clone()).placeholder(self.placeholder.clone()))
|
.child(TextElement::new(cx.entity().clone()).placeholder(self.placeholder.clone()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, relative, AnyElement, App, DefiniteLength, Entity, InteractiveElement as _,
|
div, px, relative, AnyElement, App, DefiniteLength, Entity, InteractiveElement as _,
|
||||||
IntoElement, MouseButton, ParentElement as _, Rems, RenderOnce, Styled, Window,
|
IntoElement, MouseButton, ParentElement as _, Rems, RenderOnce, StyleRefinement, Styled,
|
||||||
|
Window,
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
@@ -10,11 +11,12 @@ use crate::button::{Button, ButtonVariants as _};
|
|||||||
use crate::indicator::Indicator;
|
use crate::indicator::Indicator;
|
||||||
use crate::input::clear_button::clear_button;
|
use crate::input::clear_button::clear_button;
|
||||||
use crate::scroll::{Scrollbar, ScrollbarAxis};
|
use crate::scroll::{Scrollbar, ScrollbarAxis};
|
||||||
use crate::{h_flex, IconName, Sizable, Size, StyleSized};
|
use crate::{h_flex, IconName, Sizable, Size, StyleSized, StyledExt};
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct TextInput {
|
pub struct TextInput {
|
||||||
state: Entity<InputState>,
|
state: Entity<InputState>,
|
||||||
|
style: StyleRefinement,
|
||||||
size: Size,
|
size: Size,
|
||||||
no_gap: bool,
|
no_gap: bool,
|
||||||
prefix: Option<AnyElement>,
|
prefix: Option<AnyElement>,
|
||||||
@@ -38,6 +40,7 @@ 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,
|
no_gap: false,
|
||||||
prefix: None,
|
prefix: None,
|
||||||
@@ -276,8 +279,6 @@ impl RenderOnce for TextInput {
|
|||||||
let entity_id = self.state.entity_id();
|
let entity_id = self.state.entity_id();
|
||||||
|
|
||||||
if state.last_layout.is_some() {
|
if state.last_layout.is_some() {
|
||||||
let scroll_size = state.scroll_size;
|
|
||||||
|
|
||||||
this.relative().child(
|
this.relative().child(
|
||||||
div()
|
div()
|
||||||
.absolute()
|
.absolute()
|
||||||
@@ -290,7 +291,7 @@ impl RenderOnce for TextInput {
|
|||||||
entity_id,
|
entity_id,
|
||||||
state.scrollbar_state.clone(),
|
state.scrollbar_state.clone(),
|
||||||
state.scroll_handle.clone(),
|
state.scroll_handle.clone(),
|
||||||
scroll_size,
|
state.scroll_size,
|
||||||
)
|
)
|
||||||
.axis(ScrollbarAxis::Vertical),
|
.axis(ScrollbarAxis::Vertical),
|
||||||
),
|
),
|
||||||
@@ -299,5 +300,6 @@ impl RenderOnce for TextInput {
|
|||||||
this
|
this
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.refine_style(&self.style)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,20 @@ use std::ops::Range;
|
|||||||
|
|
||||||
use gpui::{App, Font, LineFragment, Pixels, SharedString};
|
use gpui::{App, Font, LineFragment, Pixels, SharedString};
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
pub(super) struct LineWrap {
|
||||||
|
/// The number of soft wrapped lines of this line (Not include first line.)
|
||||||
|
pub(super) wrap_lines: usize,
|
||||||
|
/// The range of the line text in the entire text.
|
||||||
|
pub(super) range: Range<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LineWrap {
|
||||||
|
pub(super) fn height(&self, line_height: Pixels) -> Pixels {
|
||||||
|
line_height * (self.wrap_lines + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 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 TextArea
|
||||||
///
|
///
|
||||||
/// After use lines to calculate the scroll size of the TextArea
|
/// After use lines to calculate the scroll size of the TextArea
|
||||||
@@ -9,6 +23,8 @@ pub(super) struct TextWrapper {
|
|||||||
pub(super) text: SharedString,
|
pub(super) text: SharedString,
|
||||||
/// The wrapped lines, value is start and end index of the line (by split \n).
|
/// The wrapped lines, value is start and end index of the line (by split \n).
|
||||||
pub(super) wrapped_lines: Vec<Range<usize>>,
|
pub(super) wrapped_lines: Vec<Range<usize>>,
|
||||||
|
/// The lines by split \n
|
||||||
|
pub(super) lines: Vec<LineWrap>,
|
||||||
pub(super) font: Font,
|
pub(super) font: Font,
|
||||||
pub(super) font_size: Pixels,
|
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
|
||||||
@@ -24,49 +40,60 @@ impl TextWrapper {
|
|||||||
font_size,
|
font_size,
|
||||||
wrap_width,
|
wrap_width,
|
||||||
wrapped_lines: Vec::new(),
|
wrapped_lines: Vec::new(),
|
||||||
|
lines: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
if self.wrap_width == wrap_width {
|
self.wrap_width = 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.wrap_width = wrap_width;
|
|
||||||
self.update(self.text.clone(), cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn set_font(&mut self, font: Font, cx: &mut App) {
|
|
||||||
self.font = font;
|
|
||||||
self.update(self.text.clone(), cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn update(&mut self, text: SharedString, cx: &mut App) {
|
|
||||||
let mut wrapped_lines = vec![];
|
let mut wrapped_lines = vec![];
|
||||||
|
let mut lines = vec![];
|
||||||
let wrap_width = self.wrap_width.unwrap_or(Pixels::MAX);
|
let wrap_width = self.wrap_width.unwrap_or(Pixels::MAX);
|
||||||
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);
|
||||||
|
|
||||||
for line in text.lines() {
|
let mut prev_line_ix = 0;
|
||||||
|
for line in text.split('\n') {
|
||||||
|
let mut line_wraps = vec![];
|
||||||
let mut prev_boundary_ix = 0;
|
let mut prev_boundary_ix = 0;
|
||||||
|
|
||||||
|
// 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 line_wrapper.wrap_line(&[LineFragment::text(line)], wrap_width) {
|
||||||
wrapped_lines.push(prev_boundary_ix..boundary.ix);
|
line_wraps.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[prev_boundary_ix..].is_empty() || prev_boundary_ix == 0 {
|
||||||
wrapped_lines.push(prev_boundary_ix..line.len());
|
wrapped_lines.push(prev_line_ix + prev_boundary_ix..prev_line_ix + line.len());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add last empty line.
|
prev_line_ix += line.len() + 1;
|
||||||
if text.chars().last().unwrap_or('\n') == '\n' {
|
|
||||||
wrapped_lines.push(text.len()..text.len());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.text = text;
|
self.text = text.clone();
|
||||||
self.wrapped_lines = wrapped_lines;
|
self.wrapped_lines = wrapped_lines;
|
||||||
|
self.lines = lines;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
use std::fmt::{self, Display, Formatter};
|
use std::fmt::{self, Display, Formatter};
|
||||||
|
|
||||||
use gpui::{div, px, App, Axis, Div, Element, ElementId, EntityId, Pixels, Styled, Window};
|
use gpui::{
|
||||||
|
div, px, App, Axis, Div, Element, ElementId, EntityId, Pixels, Refineable, StyleRefinement,
|
||||||
|
Styled, Window,
|
||||||
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
@@ -27,12 +30,20 @@ macro_rules! font_weight {
|
|||||||
|
|
||||||
/// Extends [`gpui::Styled`] with specific styling methods.
|
/// Extends [`gpui::Styled`] with specific styling methods.
|
||||||
pub trait StyledExt: Styled + Sized {
|
pub trait StyledExt: Styled + Sized {
|
||||||
|
/// Refine the style of this element, applying the given style refinement.
|
||||||
|
fn refine_style(mut self, style: &StyleRefinement) -> Self {
|
||||||
|
self.style().refine(style);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Apply self into a horizontal flex layout.
|
/// Apply self into a horizontal flex layout.
|
||||||
|
#[inline]
|
||||||
fn h_flex(self) -> Self {
|
fn h_flex(self) -> Self {
|
||||||
self.flex().flex_row().items_center()
|
self.flex().flex_row().items_center()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply self into a vertical flex layout.
|
/// Apply self into a vertical flex layout.
|
||||||
|
#[inline]
|
||||||
fn v_flex(self) -> Self {
|
fn v_flex(self) -> Self {
|
||||||
self.flex().flex_col()
|
self.flex().flex_col()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user