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:
reya
2025-07-18 09:25:55 +07:00
committed by GitHub
parent 59cfdb9ae2
commit 00b40db82c
9 changed files with 1280 additions and 1036 deletions

26
Cargo.lock generated
View File

@@ -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",

View File

@@ -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)
}); });

View File

@@ -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());
}
}

View File

@@ -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( point(
Bounds::new( bounds.left() + cursor_pos.x + line_number_width + scroll_offset.x,
point( bounds.top() + cursor_pos.y + ((line_height - cursor_height) / 2.),
bounds.left() + cursor_pos.x,
bounds.top() + cursor_pos.y + ((line_height - cursor_height) / 2.),
),
size(CURSOR_THICKNESS, cursor_height),
), ),
cx.theme().cursor, size(CURSOR_THICKNESS, cursor_height),
)) ));
}; };
} }
(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();
}); });

View File

@@ -1,380 +1,378 @@
use gpui::SharedString; use gpui::SharedString;
#[derive(Clone, PartialEq, Debug)] #[derive(Clone, PartialEq, Debug)]
pub enum MaskToken { pub enum MaskToken {
/// 0 Digit, equivalent to `[0]` /// Digit, equivalent to `[0-9]`
// Digit0, Digit,
/// Digit, equivalent to `[0-9]` /// Letter, equivalent to `[a-zA-Z]`
Digit, Letter,
/// Letter, equivalent to `[a-zA-Z]` /// Letter or digit, equivalent to `[a-zA-Z0-9]`
Letter, LetterOrDigit,
/// Letter or digit, equivalent to `[a-zA-Z0-9]` /// Separator
LetterOrDigit, Sep(char),
/// Separator /// Any character
Sep(char), Any,
/// Any character }
Any,
} #[allow(unused)]
impl MaskToken {
#[allow(unused)] /// Check if the token is any character.
impl MaskToken { pub fn is_any(&self) -> bool {
/// Check if the token is any character. matches!(self, MaskToken::Any)
pub fn is_any(&self) -> bool { }
matches!(self, MaskToken::Any)
} /// Check if the token is a match for the given character.
///
/// Check if the token is a match for the given character. /// The separator is always a match any input character.
/// fn is_match(&self, ch: char) -> bool {
/// The separator is always a match any input character. match self {
fn is_match(&self, ch: char) -> bool { MaskToken::Digit => ch.is_ascii_digit(),
match self { MaskToken::Letter => ch.is_ascii_alphabetic(),
MaskToken::Digit => ch.is_ascii_digit(), MaskToken::LetterOrDigit => ch.is_ascii_alphanumeric(),
MaskToken::Letter => ch.is_ascii_alphabetic(), MaskToken::Any => true,
MaskToken::LetterOrDigit => ch.is_ascii_alphanumeric(), MaskToken::Sep(c) => *c == ch,
MaskToken::Any => true, }
MaskToken::Sep(c) => *c == ch, }
}
} /// Is the token a separator (Can be ignored)
fn is_sep(&self) -> bool {
/// Is the token a separator (Can be ignored) matches!(self, MaskToken::Sep(_))
fn is_sep(&self) -> bool { }
matches!(self, MaskToken::Sep(_))
} /// Check if the token is a number.
pub fn is_number(&self) -> bool {
/// Check if the token is a number. matches!(self, MaskToken::Digit)
pub fn is_number(&self) -> bool { }
matches!(self, MaskToken::Digit)
} pub fn placeholder(&self) -> char {
match self {
pub fn placeholder(&self) -> char { MaskToken::Sep(c) => *c,
match self { _ => '_',
MaskToken::Sep(c) => *c, }
_ => '_', }
}
} fn mask_char(&self, ch: char) -> char {
match self {
fn mask_char(&self, ch: char) -> char { MaskToken::Digit | MaskToken::LetterOrDigit | MaskToken::Letter => ch,
match self { MaskToken::Sep(c) => *c,
MaskToken::Digit | MaskToken::LetterOrDigit | MaskToken::Letter => ch, MaskToken::Any => ch,
MaskToken::Sep(c) => *c, }
MaskToken::Any => ch, }
}
} fn unmask_char(&self, ch: char) -> Option<char> {
match self {
fn unmask_char(&self, ch: char) -> Option<char> { MaskToken::Digit => Some(ch),
match self { MaskToken::Letter => Some(ch),
MaskToken::Digit => Some(ch), MaskToken::LetterOrDigit => Some(ch),
MaskToken::Letter => Some(ch), MaskToken::Any => Some(ch),
MaskToken::LetterOrDigit => Some(ch), _ => None,
MaskToken::Any => Some(ch), }
_ => None, }
} }
}
} #[derive(Clone, Default)]
pub enum MaskPattern {
#[derive(Clone, Default)] #[default]
pub enum MaskPattern { None,
#[default] Pattern {
None, pattern: SharedString,
Pattern { tokens: Vec<MaskToken>,
pattern: SharedString, },
tokens: Vec<MaskToken>, Number {
}, /// Group separator, e.g. "," or " "
Number { separator: Option<char>,
/// Group separator, e.g. "," or " " /// Number of fraction digits, e.g. 2 for 123.45
separator: Option<char>, fraction: Option<usize>,
/// Number of fraction digits, e.g. 2 for 123.45 },
fraction: Option<usize>, }
},
} impl From<&str> for MaskPattern {
fn from(pattern: &str) -> Self {
impl From<&str> for MaskPattern { Self::new(pattern)
fn from(pattern: &str) -> Self { }
Self::new(pattern) }
}
} impl MaskPattern {
/// Create a new mask pattern
impl MaskPattern { ///
/// Create a new mask pattern /// - `9` - Digit
/// /// - `A` - Letter
/// - `9` - Digit /// - `#` - Letter or Digit
/// - `A` - Letter /// - `*` - Any character
/// - `#` - Letter or Digit /// - other characters - Separator
/// - `*` - Any character ///
/// - other characters - Separator /// For example:
/// ///
/// For example: /// - `(999)999-9999` - US phone number: (123)456-7890
/// /// - `99999-9999` - ZIP code: 12345-6789
/// - `(999)999-9999` - US phone number: (123)456-7890 /// - `AAAA-99-####` - Custom pattern: ABCD-12-3AB4
/// - `99999-9999` - ZIP code: 12345-6789 /// - `*999*` - Custom pattern: (123) or [123]
/// - `AAAA-99-####` - Custom pattern: ABCD-12-3AB4 pub fn new(pattern: &str) -> Self {
/// - `*999*` - Custom pattern: (123) or [123] let tokens = pattern
pub fn new(pattern: &str) -> Self { .chars()
let tokens = pattern .map(|ch| match ch {
.chars() // '0' => MaskToken::Digit0,
.map(|ch| match ch { '9' => MaskToken::Digit,
// '0' => MaskToken::Digit0, 'A' => MaskToken::Letter,
'9' => MaskToken::Digit, '#' => MaskToken::LetterOrDigit,
'A' => MaskToken::Letter, '*' => MaskToken::Any,
'#' => MaskToken::LetterOrDigit, _ => MaskToken::Sep(ch),
'*' => MaskToken::Any, })
_ => MaskToken::Sep(ch), .collect();
})
.collect(); Self::Pattern {
pattern: pattern.to_owned().into(),
Self::Pattern { tokens,
pattern: pattern.to_owned().into(), }
tokens, }
}
} #[allow(unused)]
fn tokens(&self) -> Option<&Vec<MaskToken>> {
#[allow(unused)] match self {
fn tokens(&self) -> Option<&Vec<MaskToken>> { Self::Pattern { tokens, .. } => Some(tokens),
match self { Self::Number { .. } => None,
Self::Pattern { tokens, .. } => Some(tokens), Self::None => None,
Self::Number { .. } => None, }
Self::None => None, }
}
} /// Create a new mask pattern with group separator, e.g. "," or " "
pub fn number(sep: Option<char>) -> Self {
/// Create a new mask pattern with group separator, e.g. "," or " " Self::Number {
pub fn number(sep: Option<char>) -> Self { separator: sep,
Self::Number { fraction: None,
separator: sep, }
fraction: None, }
}
} pub fn placeholder(&self) -> Option<String> {
match self {
pub fn placeholder(&self) -> Option<String> { Self::Pattern { tokens, .. } => {
match self { Some(tokens.iter().map(|token| token.placeholder()).collect())
Self::Pattern { tokens, .. } => { }
Some(tokens.iter().map(|token| token.placeholder()).collect()) Self::Number { .. } => None,
} Self::None => None,
Self::Number { .. } => None, }
Self::None => None, }
}
} /// Return true if the mask pattern is None or no any pattern.
pub fn is_none(&self) -> bool {
/// Return true if the mask pattern is None or no any pattern. match self {
pub fn is_none(&self) -> bool { Self::Pattern { tokens, .. } => tokens.is_empty(),
match self { Self::Number { .. } => false,
Self::Pattern { tokens, .. } => tokens.is_empty(), Self::None => true,
Self::Number { .. } => false, }
Self::None => true, }
}
} /// Check is the mask text is valid.
///
/// Check is the mask text is valid. /// If the mask pattern is None, always return true.
/// pub fn is_valid(&self, mask_text: &str) -> bool {
/// If the mask pattern is None, always return true. if self.is_none() {
pub fn is_valid(&self, mask_text: &str) -> bool { return true;
if self.is_none() { }
return true;
} let mut text_index = 0;
let mask_text_chars: Vec<char> = mask_text.chars().collect();
let mut text_index = 0; match self {
let mask_text_chars: Vec<char> = mask_text.chars().collect(); Self::Pattern { tokens, .. } => {
match self { for token in tokens {
Self::Pattern { tokens, .. } => { if text_index >= mask_text_chars.len() {
for token in tokens { break;
if text_index >= mask_text_chars.len() { }
break;
} let ch = mask_text_chars[text_index];
if token.is_match(ch) {
let ch = mask_text_chars[text_index]; text_index += 1;
if token.is_match(ch) { }
text_index += 1; }
} text_index == mask_text.len()
} }
text_index == mask_text.len() Self::Number { separator, .. } => {
} if mask_text.is_empty() {
Self::Number { separator, .. } => { return true;
if mask_text.is_empty() { }
return true;
} // check if the text is valid number
let mut parts = mask_text.split('.');
// check if the text is valid number let int_part = parts.next().unwrap_or("");
let mut parts = mask_text.split('.'); let frac_part = parts.next();
let int_part = parts.next().unwrap_or("");
let frac_part = parts.next(); if int_part.is_empty() {
return false;
if int_part.is_empty() { }
return false;
} // check if the integer part is valid
if !int_part
// check if the integer part is valid .chars()
if !int_part .all(|ch| ch.is_ascii_digit() || Some(ch) == *separator)
.chars() {
.all(|ch| ch.is_ascii_digit() || Some(ch) == *separator) return false;
{ }
return false;
} // check if the fraction part is valid
if let Some(frac) = frac_part {
// check if the fraction part is valid if !frac
if let Some(frac) = frac_part { .chars()
if !frac .all(|ch| ch.is_ascii_digit() || Some(ch) == *separator)
.chars() {
.all(|ch| ch.is_ascii_digit() || Some(ch) == *separator) return false;
{ }
return false; }
}
} true
}
true Self::None => true,
} }
Self::None => true, }
}
} /// Check if valid input char at the given position.
pub fn is_valid_at(&self, ch: char, pos: usize) -> bool {
/// Check if valid input char at the given position. if self.is_none() {
pub fn is_valid_at(&self, ch: char, pos: usize) -> bool { return true;
if self.is_none() { }
return true;
} match self {
Self::Pattern { tokens, .. } => {
match self { if let Some(token) = tokens.get(pos) {
Self::Pattern { tokens, .. } => { if token.is_match(ch) {
if let Some(token) = tokens.get(pos) { return true;
if token.is_match(ch) { }
return true;
} if token.is_sep() {
// If next token is match, it's valid
if token.is_sep() { if let Some(next_token) = tokens.get(pos + 1) {
// If next token is match, it's valid if next_token.is_match(ch) {
if let Some(next_token) = tokens.get(pos + 1) { return true;
if next_token.is_match(ch) { }
return true; }
} }
} }
}
} false
}
false Self::Number { .. } => true,
} Self::None => true,
Self::Number { .. } => true, }
Self::None => true, }
}
} /// Format the text according to the mask pattern
///
/// Format the text according to the mask pattern /// For example:
/// ///
/// For example: /// - pattern: (999)999-999
/// /// - text: 123456789
/// - pattern: (999)999-999 /// - mask_text: (123)456-789
/// - text: 123456789 pub fn mask(&self, text: &str) -> SharedString {
/// - mask_text: (123)456-789 if self.is_none() {
pub fn mask(&self, text: &str) -> SharedString { return text.to_owned().into();
if self.is_none() { }
return text.to_owned().into();
} match self {
Self::Number {
match self { separator,
Self::Number { fraction,
separator, } => {
fraction, if let Some(sep) = *separator {
} => { // Remove the existing group separator
if let Some(sep) = *separator { let text = text.replace(sep, "");
// Remove the existing group separator
let text = text.replace(sep, ""); let mut parts = text.split('.');
let int_part = parts.next().unwrap_or("");
let mut parts = text.split('.');
let int_part = parts.next().unwrap_or(""); // Limit the fraction part to the given range, if not enough, pad with 0
let frac_part = parts.next().map(|part| {
// Limit the fraction part to the given range, if not enough, pad with 0 part.chars()
let frac_part = parts.next().map(|part| { .take(fraction.unwrap_or(usize::MAX))
part.chars() .collect::<String>()
.take(fraction.unwrap_or(usize::MAX)) });
.collect::<String>()
}); // Reverse the integer part for easier grouping
let chars: Vec<char> = int_part.chars().rev().collect();
// Reverse the integer part for easier grouping let mut result = String::new();
let chars: Vec<char> = int_part.chars().rev().collect(); for (i, ch) in chars.iter().enumerate() {
let mut result = String::new(); if i > 0 && i % 3 == 0 {
for (i, ch) in chars.iter().enumerate() { result.push(sep);
if i > 0 && i % 3 == 0 { }
result.push(sep); result.push(*ch);
} }
result.push(*ch); let int_with_sep: String = result.chars().rev().collect();
}
let int_with_sep: String = result.chars().rev().collect(); let final_str = if let Some(frac) = frac_part {
if fraction == &Some(0) {
let final_str = if let Some(frac) = frac_part { int_with_sep
if fraction == &Some(0) { } else {
int_with_sep format!("{int_with_sep}.{frac}")
} else { }
format!("{int_with_sep}.{frac}") } else {
} int_with_sep
} else { };
int_with_sep return final_str.into();
}; }
return final_str.into();
} text.to_owned().into()
}
text.to_owned().into() Self::Pattern { tokens, .. } => {
} let mut result = String::new();
Self::Pattern { tokens, .. } => { let mut text_index = 0;
let mut result = String::new(); let text_chars: Vec<char> = text.chars().collect();
let mut text_index = 0; for (pos, token) in tokens.iter().enumerate() {
let text_chars: Vec<char> = text.chars().collect(); if text_index >= text_chars.len() {
for (pos, token) in tokens.iter().enumerate() { break;
if text_index >= text_chars.len() { }
break; let ch = text_chars[text_index];
} // Break if expected char is not match
let ch = text_chars[text_index]; if !token.is_sep() && !self.is_valid_at(ch, pos) {
// Break if expected char is not match break;
if !token.is_sep() && !self.is_valid_at(ch, pos) { }
break; let mask_ch = token.mask_char(ch);
} result.push(mask_ch);
let mask_ch = token.mask_char(ch); if ch == mask_ch {
result.push(mask_ch); text_index += 1;
if ch == mask_ch { continue;
text_index += 1; }
continue; }
} result.into()
} }
result.into() Self::None => text.to_owned().into(),
} }
Self::None => text.to_owned().into(), }
}
} /// Extract original text from masked text
pub fn unmask(&self, mask_text: &str) -> String {
/// Extract original text from masked text match self {
pub fn unmask(&self, mask_text: &str) -> String { Self::Number { separator, .. } => {
match self { if let Some(sep) = *separator {
Self::Number { separator, .. } => { let mut result = String::new();
if let Some(sep) = *separator { for ch in mask_text.chars() {
let mut result = String::new(); if ch == sep {
for ch in mask_text.chars() { continue;
if ch == sep { }
continue; result.push(ch);
} }
result.push(ch);
} if result.contains('.') {
result = result.trim_end_matches('0').to_string();
if result.contains('.') { }
result = result.trim_end_matches('0').to_string(); return result;
} }
return result;
} mask_text.to_owned()
}
mask_text.to_owned() Self::Pattern { tokens, .. } => {
} let mut result = String::new();
Self::Pattern { tokens, .. } => { let mask_text_chars: Vec<char> = mask_text.chars().collect();
let mut result = String::new(); for (text_index, token) in tokens.iter().enumerate() {
let mask_text_chars: Vec<char> = mask_text.chars().collect(); if text_index >= mask_text_chars.len() {
for (text_index, token) in tokens.iter().enumerate() { break;
if text_index >= mask_text_chars.len() { }
break; let ch = mask_text_chars[text_index];
} let unmask_ch = token.unmask_char(ch);
let ch = mask_text_chars[text_index]; if let Some(ch) = unmask_ch {
let unmask_ch = token.unmask_char(ch); result.push(ch);
if let Some(ch) = unmask_ch { }
result.push(ch); }
} result
} }
result Self::None => mask_text.to_owned(),
} }
Self::None => mask_text.to_owned(), }
} }
}
}

View File

@@ -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.marked_range = Some(range.start..range.start + new_text.len()); self.text_wrapper.update(&self.text, false, cx);
self.selected_range = new_selected_range_utf16 if new_text.is_empty() {
.as_ref() // Cancel selection, when cancel IME input.
.map(|range_utf16| self.range_from_utf16(range_utf16)) self.selected_range = range.start..range.start;
.map(|new_range| new_range.start + range.start..new_range.end + range.end) self.marked_range = None;
.unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len()); } else {
self.marked_range = Some(range.start..range.start + new_text.len());
self.selected_range = new_selected_range_utf16
.as_ref()
.map(|range_utf16| self.range_from_utf16(range_utf16))
.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());
}
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 let Some(p) =
line.position_for_index(range.start.saturating_sub(index_offset), line_height)
{
start_origin = Some(p + point(px(0.), y_offset));
}
if let Some(p) =
line.position_for_index(range.end.saturating_sub(index_offset), line_height)
{
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() { if start_origin.is_some() && end_origin.is_some() {
break; break;
} }
index_offset += line.len(); if start_origin.is_none() {
if let Some(p) =
line.position_for_index(range.start.saturating_sub(index_offset), line_height)
{
start_origin = Some(p + point(px(0.), y_offset));
}
}
if end_origin.is_none() {
if let Some(p) =
line.position_for_index(range.end.saturating_sub(index_offset), line_height)
{
end_origin = Some(p + point(px(0.), y_offset));
}
}
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()))
} }

View File

@@ -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)
} }
} }

View File

@@ -1,72 +1,99 @@
use std::ops::Range; use std::ops::Range;
use gpui::{App, Font, LineFragment, Pixels, SharedString}; use gpui::{App, Font, LineFragment, Pixels, SharedString};
/// Used to prepare the text with soft_wrap to be get lines to displayed in the TextArea #[allow(unused)]
/// pub(super) struct LineWrap {
/// After use lines to calculate the scroll size of the TextArea /// The number of soft wrapped lines of this line (Not include first line.)
pub(super) struct TextWrapper { pub(super) wrap_lines: usize,
pub(super) text: SharedString, /// The range of the line text in the entire text.
/// The wrapped lines, value is start and end index of the line (by split \n). pub(super) range: Range<usize>,
pub(super) wrapped_lines: Vec<Range<usize>>, }
pub(super) font: Font,
pub(super) font_size: Pixels, impl LineWrap {
/// If is none, it means the text is not wrapped pub(super) fn height(&self, line_height: Pixels) -> Pixels {
pub(super) wrap_width: Option<Pixels>, line_height * (self.wrap_lines + 1)
} }
}
#[allow(unused)]
impl TextWrapper { /// Used to prepare the text with soft_wrap to be get lines to displayed in the TextArea
pub(super) fn new(font: Font, font_size: Pixels, wrap_width: Option<Pixels>) -> Self { ///
Self { /// After use lines to calculate the scroll size of the TextArea
text: SharedString::default(), pub(super) struct TextWrapper {
font, pub(super) text: SharedString,
font_size, /// The wrapped lines, value is start and end index of the line (by split \n).
wrap_width, pub(super) wrapped_lines: Vec<Range<usize>>,
wrapped_lines: Vec::new(), /// The lines by split \n
} pub(super) lines: Vec<LineWrap>,
} pub(super) font: Font,
pub(super) font_size: Pixels,
pub(super) fn set_wrap_width(&mut self, wrap_width: Option<Pixels>, cx: &mut App) { /// If is none, it means the text is not wrapped
if self.wrap_width == wrap_width { pub(super) wrap_width: Option<Pixels>,
return; }
}
#[allow(unused)]
self.wrap_width = wrap_width; impl TextWrapper {
self.update(self.text.clone(), cx); pub(super) fn new(font: Font, font_size: Pixels, wrap_width: Option<Pixels>) -> Self {
} Self {
text: SharedString::default(),
pub(super) fn set_font(&mut self, font: Font, cx: &mut App) { font,
self.font = font; font_size,
self.update(self.text.clone(), cx); wrap_width,
} wrapped_lines: Vec::new(),
lines: Vec::new(),
pub(super) fn update(&mut self, text: SharedString, cx: &mut App) { }
let mut wrapped_lines = vec![]; }
let wrap_width = self.wrap_width.unwrap_or(Pixels::MAX);
let mut line_wrapper = cx pub(super) fn set_wrap_width(&mut self, wrap_width: Option<Pixels>, cx: &mut App) {
.text_system() self.wrap_width = wrap_width;
.line_wrapper(self.font.clone(), self.font_size); self.update(&self.text.clone(), true, cx);
}
for line in text.lines() {
let mut prev_boundary_ix = 0; pub(super) fn set_font(&mut self, font: Font, font_size: Pixels, cx: &mut App) {
for boundary in line_wrapper.wrap_line(&[LineFragment::text(line)], wrap_width) { self.font = font;
wrapped_lines.push(prev_boundary_ix..boundary.ix); self.font_size = font_size;
prev_boundary_ix = boundary.ix; self.update(&self.text.clone(), true, cx);
} }
// Reset of the line pub(super) fn update(&mut self, text: &SharedString, force: bool, cx: &mut App) {
if !line[prev_boundary_ix..].is_empty() || prev_boundary_ix == 0 { if &self.text == text && !force {
wrapped_lines.push(prev_boundary_ix..line.len()); return;
} }
}
let mut wrapped_lines = vec![];
// Add last empty line. let mut lines = vec![];
if text.chars().last().unwrap_or('\n') == '\n' { let wrap_width = self.wrap_width.unwrap_or(Pixels::MAX);
wrapped_lines.push(text.len()..text.len()); let mut line_wrapper = cx
} .text_system()
.line_wrapper(self.font.clone(), self.font_size);
self.text = text;
self.wrapped_lines = wrapped_lines; let mut prev_line_ix = 0;
} for line in text.split('\n') {
} let mut line_wraps = vec![];
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) {
line_wraps.push(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
if !line[prev_boundary_ix..].is_empty() || prev_boundary_ix == 0 {
wrapped_lines.push(prev_line_ix + prev_boundary_ix..prev_line_ix + line.len());
}
prev_line_ix += line.len() + 1;
}
self.text = text.clone();
self.wrapped_lines = wrapped_lines;
self.lines = lines;
}
}

View File

@@ -1,309 +1,320 @@
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::{
use serde::{Deserialize, Serialize}; div, px, App, Axis, Div, Element, ElementId, EntityId, Pixels, Refineable, StyleRefinement,
use theme::ActiveTheme; Styled, Window,
};
use crate::scroll::{Scrollable, ScrollbarAxis}; use serde::{Deserialize, Serialize};
use theme::ActiveTheme;
/// Returns a `Div` as horizontal flex layout.
pub fn h_flex() -> Div { use crate::scroll::{Scrollable, ScrollbarAxis};
div().h_flex()
} /// Returns a `Div` as horizontal flex layout.
pub fn h_flex() -> Div {
/// Returns a `Div` as vertical flex layout. div().h_flex()
pub fn v_flex() -> Div { }
div().v_flex()
} /// Returns a `Div` as vertical flex layout.
pub fn v_flex() -> Div {
macro_rules! font_weight { div().v_flex()
($fn:ident, $const:ident) => { }
/// [docs](https://tailwindcss.com/docs/font-weight)
fn $fn(self) -> Self { macro_rules! font_weight {
self.font_weight(gpui::FontWeight::$const) ($fn:ident, $const:ident) => {
} /// [docs](https://tailwindcss.com/docs/font-weight)
}; fn $fn(self) -> Self {
} self.font_weight(gpui::FontWeight::$const)
}
/// Extends [`gpui::Styled`] with specific styling methods. };
pub trait StyledExt: Styled + Sized { }
/// Apply self into a horizontal flex layout.
fn h_flex(self) -> Self { /// Extends [`gpui::Styled`] with specific styling methods.
self.flex().flex_row().items_center() pub trait StyledExt: Styled + Sized {
} /// Refine the style of this element, applying the given style refinement.
fn refine_style(mut self, style: &StyleRefinement) -> Self {
/// Apply self into a vertical flex layout. self.style().refine(style);
fn v_flex(self) -> Self { self
self.flex().flex_col() }
}
/// Apply self into a horizontal flex layout.
/// Render a border with a width of 1px, color ring color #[inline]
fn outline(self, _window: &Window, cx: &App) -> Self { fn h_flex(self) -> Self {
self.border_color(cx.theme().ring) self.flex().flex_row().items_center()
} }
/// Wraps the element in a ScrollView. /// Apply self into a vertical flex layout.
/// #[inline]
/// Current this is only have a vertical scrollbar. fn v_flex(self) -> Self {
fn scrollable(self, view_id: EntityId, axis: ScrollbarAxis) -> Scrollable<Self> self.flex().flex_col()
where }
Self: Element,
{ /// Render a border with a width of 1px, color ring color
Scrollable::new(view_id, self, axis) fn outline(self, _window: &Window, cx: &App) -> Self {
} self.border_color(cx.theme().ring)
}
font_weight!(font_thin, THIN);
font_weight!(font_extralight, EXTRA_LIGHT); /// Wraps the element in a ScrollView.
font_weight!(font_light, LIGHT); ///
font_weight!(font_normal, NORMAL); /// Current this is only have a vertical scrollbar.
font_weight!(font_medium, MEDIUM); fn scrollable(self, view_id: EntityId, axis: ScrollbarAxis) -> Scrollable<Self>
font_weight!(font_semibold, SEMIBOLD); where
font_weight!(font_bold, BOLD); Self: Element,
font_weight!(font_extrabold, EXTRA_BOLD); {
font_weight!(font_black, BLACK); Scrollable::new(view_id, self, axis)
}
/// Set as Popover style
fn popover_style(self, cx: &mut App) -> Self { font_weight!(font_thin, THIN);
self.bg(cx.theme().background) font_weight!(font_extralight, EXTRA_LIGHT);
.border_1() font_weight!(font_light, LIGHT);
.border_color(cx.theme().border) font_weight!(font_normal, NORMAL);
.shadow_lg() font_weight!(font_medium, MEDIUM);
.rounded_lg() font_weight!(font_semibold, SEMIBOLD);
} font_weight!(font_bold, BOLD);
} font_weight!(font_extrabold, EXTRA_BOLD);
font_weight!(font_black, BLACK);
impl<E: Styled> StyledExt for E {}
/// Set as Popover style
/// A size for elements. fn popover_style(self, cx: &mut App) -> Self {
#[derive(Clone, Default, Copy, PartialEq, Eq, Debug, Deserialize, Serialize)] self.bg(cx.theme().background)
pub enum Size { .border_1()
Size(Pixels), .border_color(cx.theme().border)
XSmall, .shadow_lg()
Small, .rounded_lg()
#[default] }
Medium, }
Large,
} impl<E: Styled> StyledExt for E {}
impl From<Pixels> for Size { /// A size for elements.
fn from(size: Pixels) -> Self { #[derive(Clone, Default, Copy, PartialEq, Eq, Debug, Deserialize, Serialize)]
Size::Size(size) pub enum Size {
} Size(Pixels),
} XSmall,
Small,
/// A trait for defining element that can be selected. #[default]
pub trait Selectable: Sized { Medium,
fn element_id(&self) -> &ElementId; Large,
/// Set the selected state of the element. }
fn selected(self, selected: bool) -> Self;
} impl From<Pixels> for Size {
fn from(size: Pixels) -> Self {
/// A trait for defining element that can be disabled. Size::Size(size)
pub trait Disableable { }
/// Set the disabled state of the element. }
fn disabled(self, disabled: bool) -> Self;
} /// A trait for defining element that can be selected.
pub trait Selectable: Sized {
/// A trait for setting the size of an element. fn element_id(&self) -> &ElementId;
pub trait Sizable: Sized { /// Set the selected state of the element.
/// Set the ui::Size of this element. fn selected(self, selected: bool) -> Self;
/// }
/// Also can receive a `ButtonSize` to convert to `IconSize`,
/// Or a `Pixels` to set a custom size: `px(30.)` /// A trait for defining element that can be disabled.
fn with_size(self, size: impl Into<Size>) -> Self; pub trait Disableable {
/// Set the disabled state of the element.
/// Set to Size::XSmall fn disabled(self, disabled: bool) -> Self;
fn xsmall(self) -> Self { }
self.with_size(Size::XSmall)
} /// A trait for setting the size of an element.
pub trait Sizable: Sized {
/// Set to Size::Small /// Set the ui::Size of this element.
fn small(self) -> Self { ///
self.with_size(Size::Small) /// Also can receive a `ButtonSize` to convert to `IconSize`,
} /// Or a `Pixels` to set a custom size: `px(30.)`
fn with_size(self, size: impl Into<Size>) -> Self;
/// Set to Size::Medium
fn medium(self) -> Self { /// Set to Size::XSmall
self.with_size(Size::Medium) fn xsmall(self) -> Self {
} self.with_size(Size::XSmall)
}
/// Set to Size::Large
fn large(self) -> Self { /// Set to Size::Small
self.with_size(Size::Large) fn small(self) -> Self {
} self.with_size(Size::Small)
} }
#[allow(unused)] /// Set to Size::Medium
pub trait StyleSized<T: Styled> { fn medium(self) -> Self {
fn input_font_size(self, size: Size) -> Self; self.with_size(Size::Medium)
fn input_size(self, size: Size) -> Self; }
fn input_pl(self, size: Size) -> Self;
fn input_pr(self, size: Size) -> Self; /// Set to Size::Large
fn input_px(self, size: Size) -> Self; fn large(self) -> Self {
fn input_py(self, size: Size) -> Self; self.with_size(Size::Large)
fn input_h(self, size: Size) -> Self; }
fn list_size(self, size: Size) -> Self; }
fn list_px(self, size: Size) -> Self;
fn list_py(self, size: Size) -> Self; #[allow(unused)]
/// Apply size with the given `Size`. pub trait StyleSized<T: Styled> {
fn size_with(self, size: Size) -> Self; fn input_font_size(self, size: Size) -> Self;
} fn input_size(self, size: Size) -> Self;
fn input_pl(self, size: Size) -> Self;
impl<T: Styled> StyleSized<T> for T { fn input_pr(self, size: Size) -> Self;
fn input_font_size(self, size: Size) -> Self { fn input_px(self, size: Size) -> Self;
match size { fn input_py(self, size: Size) -> Self;
Size::XSmall => self.text_xs(), fn input_h(self, size: Size) -> Self;
Size::Small => self.text_sm(), fn list_size(self, size: Size) -> Self;
Size::Medium => self.text_base(), fn list_px(self, size: Size) -> Self;
Size::Large => self.text_lg(), fn list_py(self, size: Size) -> Self;
Size::Size(size) => self.text_size(size), /// Apply size with the given `Size`.
} fn size_with(self, size: Size) -> Self;
} }
fn input_size(self, size: Size) -> Self { impl<T: Styled> StyleSized<T> for T {
self.input_px(size).input_py(size).input_h(size) fn input_font_size(self, size: Size) -> Self {
} match size {
Size::XSmall => self.text_xs(),
fn input_pl(self, size: Size) -> Self { Size::Small => self.text_sm(),
match size { Size::Medium => self.text_base(),
Size::Large => self.pl_5(), Size::Large => self.text_lg(),
Size::Medium => self.pl_3(), Size::Size(size) => self.text_size(size),
_ => self.pl_2(), }
} }
}
fn input_size(self, size: Size) -> Self {
fn input_pr(self, size: Size) -> Self { self.input_px(size).input_py(size).input_h(size)
match size { }
Size::Large => self.pr_5(),
Size::Medium => self.pr_3(), fn input_pl(self, size: Size) -> Self {
_ => self.pr_2(), match size {
} Size::Large => self.pl_5(),
} Size::Medium => self.pl_3(),
_ => self.pl_2(),
fn input_px(self, size: Size) -> Self { }
match size { }
Size::Large => self.px_5(),
Size::Medium => self.px_3(), fn input_pr(self, size: Size) -> Self {
_ => self.px_2(), match size {
} Size::Large => self.pr_5(),
} Size::Medium => self.pr_3(),
_ => self.pr_2(),
fn input_py(self, size: Size) -> Self { }
match size { }
Size::Large => self.py_5(),
Size::Medium => self.py_2(), fn input_px(self, size: Size) -> Self {
_ => self.py_1(), match size {
} Size::Large => self.px_5(),
} Size::Medium => self.px_3(),
_ => self.px_2(),
fn input_h(self, size: Size) -> Self { }
match size { }
Size::XSmall => self.h_7(),
Size::Small => self.h_8(), fn input_py(self, size: Size) -> Self {
Size::Medium => self.h_9(), match size {
Size::Large => self.h_12(), Size::Large => self.py_5(),
_ => self.h(px(24.)), Size::Medium => self.py_2(),
} _ => self.py_1(),
.input_font_size(size) }
} }
fn list_size(self, size: Size) -> Self { fn input_h(self, size: Size) -> Self {
self.list_px(size).list_py(size).input_font_size(size) match size {
} Size::XSmall => self.h_7(),
Size::Small => self.h_8(),
fn list_px(self, size: Size) -> Self { Size::Medium => self.h_9(),
match size { Size::Large => self.h_12(),
Size::Small => self.px_2(), _ => self.h(px(24.)),
_ => self.px_3(), }
} .input_font_size(size)
} }
fn list_py(self, size: Size) -> Self { fn list_size(self, size: Size) -> Self {
match size { self.list_px(size).list_py(size).input_font_size(size)
Size::Large => self.py_2(), }
Size::Medium => self.py_1(),
Size::Small => self.py_0p5(), fn list_px(self, size: Size) -> Self {
_ => self.py_1(), match size {
} Size::Small => self.px_2(),
} _ => self.px_3(),
}
fn size_with(self, size: Size) -> Self { }
match size {
Size::Large => self.size_11(), fn list_py(self, size: Size) -> Self {
Size::Medium => self.size_8(), match size {
Size::Small => self.size_5(), Size::Large => self.py_2(),
Size::XSmall => self.size_4(), Size::Medium => self.py_1(),
Size::Size(size) => self.size(size), Size::Small => self.py_0p5(),
} _ => self.py_1(),
} }
} }
pub trait AxisExt { fn size_with(self, size: Size) -> Self {
fn is_horizontal(&self) -> bool; match size {
fn is_vertical(&self) -> bool; Size::Large => self.size_11(),
} Size::Medium => self.size_8(),
Size::Small => self.size_5(),
impl AxisExt for Axis { Size::XSmall => self.size_4(),
fn is_horizontal(&self) -> bool { Size::Size(size) => self.size(size),
self == &Axis::Horizontal }
} }
}
fn is_vertical(&self) -> bool {
self == &Axis::Vertical pub trait AxisExt {
} fn is_horizontal(&self) -> bool;
} fn is_vertical(&self) -> bool;
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Placement { impl AxisExt for Axis {
Top, fn is_horizontal(&self) -> bool {
Bottom, self == &Axis::Horizontal
Left, }
Right,
} fn is_vertical(&self) -> bool {
self == &Axis::Vertical
impl Display for Placement { }
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { }
match self {
Placement::Top => write!(f, "Top"), #[derive(Clone, Copy, PartialEq, Eq, Debug)]
Placement::Bottom => write!(f, "Bottom"), pub enum Placement {
Placement::Left => write!(f, "Left"), Top,
Placement::Right => write!(f, "Right"), Bottom,
} Left,
} Right,
} }
impl Placement { impl Display for Placement {
pub fn is_horizontal(&self) -> bool { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
matches!(self, Placement::Left | Placement::Right) match self {
} Placement::Top => write!(f, "Top"),
Placement::Bottom => write!(f, "Bottom"),
pub fn is_vertical(&self) -> bool { Placement::Left => write!(f, "Left"),
matches!(self, Placement::Top | Placement::Bottom) Placement::Right => write!(f, "Right"),
} }
}
pub fn axis(&self) -> Axis { }
match self {
Placement::Top | Placement::Bottom => Axis::Vertical, impl Placement {
Placement::Left | Placement::Right => Axis::Horizontal, pub fn is_horizontal(&self) -> bool {
} matches!(self, Placement::Left | Placement::Right)
} }
}
pub fn is_vertical(&self) -> bool {
/// A enum for defining the side of the element. matches!(self, Placement::Top | Placement::Bottom)
#[derive(Clone, Copy, PartialEq, Eq, Debug)] }
pub enum Side {
Left, pub fn axis(&self) -> Axis {
Right, match self {
} Placement::Top | Placement::Bottom => Axis::Vertical,
Placement::Left | Placement::Right => Axis::Horizontal,
impl Side { }
pub(crate) fn is_left(&self) -> bool { }
matches!(self, Self::Left) }
}
} /// A enum for defining the side of the element.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
/// A trait for defining element that can be collapsed. pub enum Side {
pub trait Collapsible { Left,
fn collapsed(self, collapsed: bool) -> Self; Right,
fn is_collapsed(&self) -> bool; }
}
impl Side {
pub(crate) fn is_left(&self) -> bool {
matches!(self, Self::Left)
}
}
/// A trait for defining element that can be collapsed.
pub trait Collapsible {
fn collapsed(self, collapsed: bool) -> Self;
fn is_collapsed(&self) -> bool;
}