documents: right presses keep a selection; Delete Image
CI / platforms (windows-latest) (push) Failing after 9s
CI / site (push) Successful in 21s
CI / crate (push) Failing after 12m8s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s

A right press in a text field or the code editor keeps a selection it
lands in, else moves the caret. In visual mode a right press in the open
block keeps it open and emits Pointed, read apart from the whole text.
MarkdownDocument::delete_image removes the picture at the caret.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-14 18:15:38 -04:00
1 parent 6c2d28c56e
commit 013f14216f
9 files changed
+284 -5

No files matched your search

+1 -1
View File
@@ -13,7 +13,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
- Web views: `wry` 0.57 (Apache-2.0 OR MIT, default features off, macOS only) lays a child WKWebView over the window.
- Terminal: `alacritty_terminal` 0.26 (Apache-2.0, default features off, native only) for the grid, its parser and the pseudo-terminal; `futures` carries its events. `vte` 0.15 (Apache-2.0 OR MIT), the parser it re-exports, reads `AnsiText`'s codes alone, so colored output builds for wasm32.
- Diffs: `similar` 3.2 (Apache-2.0) for line and word diffs and three-way merges; its `unicode` feature splits words at punctuation.
- Markdown: `markdown` 1.0.0 (markdown-rs, MIT) reads GitHub's flavor and math into a syntax tree with byte positions for `MarkdownRenderer`, `MarkdownEditor`, the highlights, block ranges and outline. `MarkdownRenderer` draws display math through `typography::Latex`, a ```` ```mermaid ```` block through `documents::Diagram`, and pictures from the owner's folder (`base`) or `data:` addresses (`base64` 0.22, MIT OR Apache-2.0); web pictures stay unloaded. Inline math shows as TeX: gpui text carries no inline pictures. Inline it reads as Typora's defaults: `:shortcode:` turns to its emoji as GitHub reads it (the `emojis` crate's names, GitHub's today, so `:cricket:` is the insect; read after escapes and references, so `\:smile:` turns too; inside an autolink or `<code>` it stays text; within one DOM text node, which filtered tags do not split, the first `:name:`-shaped run decides for all: blank text or a space before it turns every shortcode, else none, so `x:smile: :smile:` stays), inline HTML's `u`, `b`/`strong`, `i`/`em`, `s`/`del`/`strike`, `code`/`kbd`, `mark`, `br` and `a href` draw as what they mean GFM's filtered tags (`script`, `style`, `title`, `textarea`, `iframe`, `xmp`, `noembed`, `noframes`, `plaintext`) show as text, and other tags, hyphenated names too, vanish, leaving their text; a tag's name runs to white space or `/`, as HTML5 reads it; attribute values split on ASCII whitespace alone and decode as HTML decodes them; each Markdown container is the element GitHub writes (`em`, `strong`, `del`, `a`), on one stack with the tags and repaired as HTML5 repairs: an end tag closes the innermost element of its name and all above it, of which only formatting elements (`a`, `b`, `code`, `em`, `i`, `s`, `strike`, `strong`, `u`) reopen, a new `a` ends the open one and an `a` without `href` links nothing; other tags are not tracked, and `</br>` breaks as `<br>` does; and a footnote reference is its number in superscript, numbered by first reference, as its definition is; one `~` still strikes. At the root, blocks read as GitHub shows them: a quote whose first line is plain text reading `[!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]` or `[!CAUTION]` in any case, after escapes and references, spaces and tabs around it, with more after it, is an alert under its title (Important in the palette's violet, `syntax.keyword`, read at 4.5:1); a paragraph that is `[toc]` alone, as Typora reads it, lists every heading by depth, and a press on one reveals it as a link press does (`MarkdownDocument`'s `reveal`), the heading found where `CodeEditor` anchors carried it through each edit since the read, gone if an edit or an undo took it, and a secondary press acts on press, since a block writing back moves the list; front matter shows in a box without its fences; an HTML block shows its text with the inline tags above, white space collapsed and owed as a browser lays it, block elements breaking lines and closing what opened inside them, HTML5's start tags ending an open `p`, `fieldset` and `menu` too, and an `li` start an `li`, a `dd` or `dt` start either, as HTML5 ends them, comments dropped, the filtered tags shown as text, and an unclosed tag dropping the rest; a rule is a 2px `border_strong` line; bullets go •, ◦, ▪ by list depth, ordered lists counted. Exports read a tree as the drawing does through `MarkdownReader` (`block` gives its `MarkdownBlock`s, `inline` its text, `note` and `label` its footnotes, `headings` the `[toc]` list), one fresh stack per paragraph, heading or cell. `MarkdownDocument` holds a whole file in one `CodeEditor` (text and undo, colored by `CodeEditor::highlighter` with `markdown_highlights`): visual draws the file's top-level blocks in a gpui `list`, a pressed block opening as its source in a `TextInput` whose typing is the document's text at once and whose write-back is one undo step; source is the editor; preview draws without presses. A written-back block, a mode switch or a new text is read at once up to 128 KiB, longer ones on the background executor; typing in source or split reads 300 ms after the last key, off the main thread (markdown-rs grows with the square of its nodes, not its bytes: in release, 128 KiB of one-letter paragraphs takes 1.7 s, 16 KiB 42 ms, while 128 KiB of prose takes 20 ms; a block-heavy text under the cutoff can still hold the UI thread past 100 ms, a known limit). Texts past `LARGEST_READ` (2 MiB) are never read: no blocks, no outline. A new read keeps the list's blocks alike at both ends. One caret serves every mode: the editor's selection, which a written-back block moves; `set_caret` and `set_top_line` set caret and view apart; a top line asked (numbered, so a late frame callback finishes only its own ask; new text places it again, clamped to the text as it then is) waits for the read, lands on its block, then a frame later at its share of the block's height; `reveal` is such an ask with the caret at its line; a switch between source and the listLine truncated
- Markdown: `markdown` 1.0.0 (markdown-rs, MIT) reads GitHub's flavor and math into a syntax tree with byte positions for `MarkdownRenderer`, `MarkdownEditor`, the highlights, block ranges and outline. `MarkdownRenderer` draws display math through `typography::Latex`, a ```` ```mermaid ```` block through `documents::Diagram`, and pictures from the owner's folder (`base`) or `data:` addresses (`base64` 0.22, MIT OR Apache-2.0); web pictures stay unloaded. Inline math shows as TeX: gpui text carries no inline pictures. Inline it reads as Typora's defaults: `:shortcode:` turns to its emoji as GitHub reads it (the `emojis` crate's names, GitHub's today, so `:cricket:` is the insect; read after escapes and references, so `\:smile:` turns too; inside an autolink or `<code>` it stays text; within one DOM text node, which filtered tags do not split, the first `:name:`-shaped run decides for all: blank text or a space before it turns every shortcode, else none, so `x:smile: :smile:` stays), inline HTML's `u`, `b`/`strong`, `i`/`em`, `s`/`del`/`strike`, `code`/`kbd`, `mark`, `br` and `a href` draw as what they mean GFM's filtered tags (`script`, `style`, `title`, `textarea`, `iframe`, `xmp`, `noembed`, `noframes`, `plaintext`) show as text, and other tags, hyphenated names too, vanish, leaving their text; a tag's name runs to white space or `/`, as HTML5 reads it; attribute values split on ASCII whitespace alone and decode as HTML decodes them; each Markdown container is the element GitHub writes (`em`, `strong`, `del`, `a`), on one stack with the tags and repaired as HTML5 repairs: an end tag closes the innermost element of its name and all above it, of which only formatting elements (`a`, `b`, `code`, `em`, `i`, `s`, `strike`, `strong`, `u`) reopen, a new `a` ends the open one and an `a` without `href` links nothing; other tags are not tracked, and `</br>` breaks as `<br>` does; and a footnote reference is its number in superscript, numbered by first reference, as its definition is; one `~` still strikes. At the root, blocks read as GitHub shows them: a quote whose first line is plain text reading `[!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]` or `[!CAUTION]` in any case, after escapes and references, spaces and tabs around it, with more after it, is an alert under its title (Important in the palette's violet, `syntax.keyword`, read at 4.5:1); a paragraph that is `[toc]` alone, as Typora reads it, lists every heading by depth, and a press on one reveals it as a link press does (`MarkdownDocument`'s `reveal`), the heading found where `CodeEditor` anchors carried it through each edit since the read, gone if an edit or an undo took it, and a secondary press acts on press, since a block writing back moves the list; front matter shows in a box without its fences; an HTML block shows its text with the inline tags above, white space collapsed and owed as a browser lays it, block elements breaking lines and closing what opened inside them, HTML5's start tags ending an open `p`, `fieldset` and `menu` too, and an `li` start an `li`, a `dd` or `dt` start either, as HTML5 ends them, comments dropped, the filtered tags shown as text, and an unclosed tag dropping the rest; a rule is a 2px `border_strong` line; bullets go •, ◦, ▪ by list depth, ordered lists counted. Exports read a tree as the drawing does through `MarkdownReader` (`block` gives its `MarkdownBlock`s, `inline` its text, `note` and `label` its footnotes, `headings` the `[toc]` list), one fresh stack per paragraph, heading or cell. `MarkdownDocument` holds a whole file in one `CodeEditor` (text and undo, colored by `CodeEditor::highlighter` with `markdown_highlights`): visual draws the file's top-level blocks in a gpui `list`, a pressed block opening as its source in a `TextInput` whose typing is the document's text at once and whose write-back is one undo step; source is the editor; preview draws without presses. A written-back block, a mode switch or a new text is read at once up to 128 KiB, longer ones on the background executor; typing in source or split reads 300 ms after the last key, off the main thread (markdown-rs grows with the square of its nodes, not its bytes: in release, 128 KiB of one-letter paragraphs takes 1.7 s, 16 KiB 42 ms, while 128 KiB of prose takes 20 ms; a block-heavy text under the cutoff can still hold the UI thread past 100 ms, a known limit). Texts past `LARGEST_READ` (2 MiB) are never read: no blocks, no outline. A new read keeps the list's blocks alike at both ends. One caret serves every mode: the editor's selection, which a written-back block moves; `set_caret` and `set_top_line` set caret and view apart; a top line asked (numbered, so a late frame callback finishes only its own ask; new text places it again, clamped to the text as it then is) waits for the read, lands on its block, then a frame later at its share of the block's height; `reveal` is such an ask with the caret at its line; a switch between source and the listLine truncated
- Pictures: `image` 0.25 (MIT OR Apache-2.0), the crate gpui decodes with, default features off; it turns decoded frames and rims map tiles.
- Math and diagrams: RaTeX 0.1.14 (`ratex-parser`, `ratex-layout`, `ratex-svg` with `embed-fonts`, `ratex-types`; MIT, KaTeX's fonts OFL) typesets `Latex` to SVG with outlined glyphs; `mermaid-rs-renderer` 0.3.1 (MIT, default features off) draws `Diagram`. Both go through gpui's `SvgRenderer` at the window's scale, loaded as gpui assets off the main thread; `usvg` 0.46, gpui's own, reads a diagram's size. A RaTeX or Mermaid panic on parsed input becomes the element's error. Each element holds its picture through `primitives::held`: a new source, or the element leaving the window, removes the old asset from gpui's cache and drops its image from the atlas after the frame.
- Maps: Natural Earth's 110m countries (public domain). `assets/maps/countries.geojson`, trimmed by `scripts/countries.py` to an ISO code, a name and positions to 0.01°, backs `maps::WorldMap`; the gallery's tiles are cut from the same file, z0 to z3, by `scripts/tiles.py` with Pillow.
+7 -2
View File
@@ -7,7 +7,7 @@ use gpui::{AppContext as _, Context};
use super::{
DocumentEvent, LARGEST_READ, MarkdownDocument,
block::{self, BlockCommand, settled},
format::{Change, EditRefusal, InlineFormat, toggle},
format::{Change, EditRefusal, InlineFormat, drop_image, toggle},
tables::{self, TableEdit},
};
use crate::documents::{MarkdownMode, modes::joint};
@@ -29,6 +29,11 @@ impl MarkdownDocument {
)
}
/// Removes the picture at the caret, read apart.
pub fn delete_image(&mut self, cx: &mut Context<Self>) -> Result<(), EditRefusal> {
self.apart("delete image".to_string(), drop_image, cx)
}
/// `command` at the caret, read apart; refusals by event.
pub fn block(
&mut self,
@@ -107,7 +112,7 @@ impl MarkdownDocument {
}
/// The text an edit reads: the open block's in place.
fn snapshot(&self, cx: &Context<Self>) -> Snapshot {
pub(super) fn snapshot(&self, cx: &Context<Self>) -> Snapshot {
let text = self.editor.read(cx).text();
let Some(open) = &self.open else {
let picked = self.editor.read(cx).primary().range();
+19
View File
@@ -40,6 +40,8 @@ pub enum EditRefusal {
Outside,
/// A table holds the caret.
Table,
/// No picture holds the caret.
NoImage,
/// Front matter is there already.
Present,
/// The selection crosses a block or another mark.
@@ -59,6 +61,23 @@ pub enum EditRefusal {
/// The edits and the selection after them.
pub(super) type Change = (Vec<(Range<usize>, String)>, Range<usize>);
/// Removes the picture at the caret, as Typora's Delete Image.
pub(super) fn drop_image(text: &str, selection: Range<usize>) -> Result<Change, EditRefusal> {
let root = file_tree(text);
let mut nodes = Vec::new();
walk(&root, &mut nodes);
let at = selection.start;
let picture = (nodes.iter())
.filter(|node| matches!(node, Node::Image(_) | Node::ImageReference(_)))
.map(|node| span(node))
.find(|range| range.contains(&at))
.ok_or(EditRefusal::NoImage)?;
Ok((
vec![(picture.clone(), String::new())],
picture.start..picture.start,
))
}
/// Typora's word stops: punctuation and space.
fn stop(ch: char) -> bool {
ch.is_whitespace() || "()[]^&_-+*#@!~`\\/{}'\":;<>,.?%".contains(ch)
+76 -2
View File
@@ -1,10 +1,11 @@
use std::rc::Rc;
use gpui::{Context, MouseButton, Window};
use gpui::{AppContext as _, Context, MouseButton, Window};
use markdown::mdast;
use super::{DocumentEvent, MarkdownDocument, edit::Landing, edit::moved, parse::Block};
use crate::documents::{
markdown::{file_tree, span, walk},
render::{OnPoint, Place, Pointed},
tree::{Node, alert, is_toc, lines_at},
};
@@ -54,7 +55,7 @@ impl TargetBlock {
/// What a secondary press landed on.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DocumentTarget {
/// The source byte the caret went to.
/// The caret's source byte; an open block's as unwritten.
pub at: usize,
/// None past the last block.
pub block: Option<TargetBlock>,
@@ -160,4 +161,77 @@ impl MarkdownDocument {
}));
cx.notify();
}
/// A right press in the open block; it stays open.
pub(super) fn point_open(&mut self, cx: &mut Context<Self>) {
if self.open.is_none() {
return log::warn!("markdown document: a press on a block closed since");
}
let taken = self.snapshot(cx);
let (full, _, selection) = taken.clone();
cx.spawn(async move |document, cx| {
let target = cx
.background_spawn(async move { target_at(&full, selection.start) })
.await;
document
.update(cx, |document, cx| {
if document.open.is_none() || document.snapshot(cx) != taken {
return log::info!("markdown document: the pressed block changed first");
}
log::info!("markdown document: a secondary press in the open block");
cx.emit(DocumentEvent::Pointed(target));
})
.ok();
})
.detach();
}
}
/// What byte `at` of `text` holds.
fn target_at(text: &str, at: usize) -> DocumentTarget {
let root = file_tree(text);
let mut nodes = Vec::new();
walk(&root, &mut nodes);
let block = (root
.children()
.map(Vec::as_slice)
.unwrap_or_default()
.iter())
.find(|node| span(node).start <= at && at <= span(node).end)
.map(|node| TargetBlock::of(node, text));
let around: Vec<&mdast::Node> = (nodes.iter().copied())
.filter(|node| span(node).contains(&at))
.collect();
let defined = |identifier: &str| {
nodes.iter().find_map(|node| match node {
mdast::Node::Definition(definition) if definition.identifier == identifier => {
Some(definition.url.clone())
}
_ => None,
})
};
let link = around.iter().rev().find_map(|node| match node {
mdast::Node::Link(link) => Some(link.url.clone()),
mdast::Node::LinkReference(link) => defined(&link.identifier),
_ => None,
});
let image = around.iter().rev().find_map(|node| match node {
mdast::Node::Image(picture) => Some(picture.url.clone()),
mdast::Node::ImageReference(picture) => defined(&picture.identifier),
_ => None,
});
let code = around.iter().find_map(|node| match node {
mdast::Node::Code(code) => Some(code.value.clone()),
_ => None,
});
DocumentTarget {
at,
block,
link,
image,
in_table: around
.iter()
.any(|node| matches!(node, mdast::Node::Table(_))),
code,
}
}
+1
View File
@@ -377,6 +377,7 @@ mod place;
mod point;
mod presses;
mod reading;
mod secondary;
mod toc;
mod tools;
+126
View File
@@ -0,0 +1,126 @@
//! Right presses that keep a selection; Delete Image (R4.10).
use gpui::{Entity, Modifiers, MouseButton, TestAppContext, VisualTestContext, point, px};
use super::{MarkdownMode, open, opened, point::heard, point::press_on, text};
use crate::documents::{DocumentTarget, EditRefusal, MarkdownDocument, TargetBlock};
/// A right press near the left of the open block's text.
fn press_open(cx: &mut VisualTestContext) {
let field = cx.debug_bounds("markdown-open").expect("a block is open");
let at = point(field.left() + px(20.0), field.top() + px(14.0));
cx.simulate_mouse_down(at, MouseButton::Right, Modifiers::none());
cx.simulate_mouse_up(at, MouseButton::Right, Modifiers::none());
cx.run_until_parked();
}
fn open_selection(
document: &Entity<MarkdownDocument>,
cx: &mut VisualTestContext,
) -> Option<std::ops::Range<usize>> {
document.read_with(cx, |document, cx| {
(document.open.as_ref()).map(|open| open.input.read(cx).selection())
})
}
fn select_open(
document: &Entity<MarkdownDocument>,
range: std::ops::Range<usize>,
cx: &mut VisualTestContext,
) {
document.update(cx, |document, cx| {
let input = document.open.as_ref().expect("open").input.clone();
input.update(cx, |input, cx| input.select(range, cx));
});
}
const LINKED: &str = "[docs](https://x.test)\n\nNext.\n";
#[gpui::test]
fn a_right_press_in_a_selection_keeps_the_block_and_the_selection(cx: &mut TestAppContext) {
let (document, cx) = open(LINKED, MarkdownMode::Visual, cx);
let heard = heard(&document, cx);
press_on(0, 1, MouseButton::Left, cx);
select_open(&document, 0..22, cx);
press_open(cx);
assert_eq!(
open_selection(&document, cx),
Some(0..22),
"kept open and selected"
);
let target = DocumentTarget {
link: Some("https://x.test".into()),
..super::point::target(0, TargetBlock::Paragraph)
};
assert_eq!(*heard.borrow(), [target]);
assert_eq!(text(&document, cx), LINKED, "nothing written");
}
#[gpui::test]
fn a_right_press_outside_the_selection_moves_the_caret(cx: &mut TestAppContext) {
let (document, cx) = open(LINKED, MarkdownMode::Visual, cx);
press_on(0, 1, MouseButton::Left, cx);
select_open(&document, 22..22, cx);
press_open(cx);
let at = open_selection(&document, cx).expect("still open");
assert!(
at.is_empty() && at.start < 5,
"the caret went to the press: {at:?}"
);
}
#[gpui::test]
fn a_reference_in_the_open_block_reads_its_definition_outside(cx: &mut TestAppContext) {
let source = "[docs][d]\n\n[d]: https://x.test\n";
let (document, cx) = open(source, MarkdownMode::Visual, cx);
let heard = heard(&document, cx);
press_on(0, 1, MouseButton::Left, cx);
select_open(&document, 0..9, cx);
press_open(cx);
let link = heard
.borrow()
.first()
.and_then(|target| target.link.clone());
assert_eq!(link.as_deref(), Some("https://x.test"));
assert_eq!(opened(&document, cx), "[docs][d]");
}
#[gpui::test]
fn a_right_press_in_source_keeps_the_selection(cx: &mut TestAppContext) {
let (document, cx) = open("alpha beta\n", MarkdownMode::Source, cx);
let editor = document.read_with(cx, |document, _| document.editor().clone());
editor.update(cx, |editor, cx| editor.select(std::iter::once(0..10), cx));
let line = cx.debug_bounds("editor-line-0").expect("drawn");
let at = point(line.left() + px(60.0), line.top() + px(6.0));
cx.simulate_mouse_down(at, MouseButton::Right, Modifiers::none());
cx.run_until_parked();
let kept = editor.read_with(cx, |editor, _| editor.primary().range());
assert_eq!(kept, 0..10);
editor.update(cx, |editor, cx| editor.select(std::iter::once(10..10), cx));
cx.simulate_mouse_down(at, MouseButton::Right, Modifiers::none());
cx.run_until_parked();
let moved = editor.read_with(cx, |editor, _| editor.primary().range());
assert!(moved.is_empty() && moved.start < 10, "{moved:?}");
}
#[gpui::test]
fn delete_image_takes_the_picture_away_in_one_step(cx: &mut TestAppContext) {
let source = "A ![p](p.png) B\n";
let (document, cx) = open(source, MarkdownMode::Source, cx);
let heard = super::format::refusals(&document, cx);
document.update(cx, |document, cx| {
document.set_caret(0, 0, cx);
document.delete_image(cx).expect("taken");
});
cx.run_until_parked();
assert_eq!(*heard.borrow(), [EditRefusal::NoImage]);
document.update(cx, |document, cx| {
document.set_caret(0, 4, cx);
document.delete_image(cx).expect("taken");
});
cx.run_until_parked();
assert_eq!(text(&document, cx), "A B\n");
let editor = document.read_with(cx, |document, _| document.editor().clone());
editor.update(cx, |editor, cx| editor.undo(cx));
assert_eq!(text(&document, cx), source);
}
+7
View File
@@ -72,8 +72,15 @@ impl MarkdownDocument {
// Focus fades all but the reader's block.
let faded = self.look.focus && editing_mode(self.mode) && ix != self.current_block(cx);
let open = self.open.as_ref();
let point = cx.listener(|document, _: &MouseDownEvent, window, cx| {
window.prevent_default();
cx.stop_propagation();
document.point_open(cx);
});
let field = |open: &super::Open| {
div()
.debug_selector(|| "markdown-open".to_string())
.on_mouse_down(MouseButton::Right, point)
.text_size(body)
.px_3()
.py_2()
+27
View File
@@ -335,6 +335,12 @@ impl CodeEditor {
editor.press(line, event, window, cx)
}),
)
.on_mouse_down(
MouseButton::Right,
cx.listener(move |editor, event: &MouseDownEvent, window, cx| {
editor.secondary_press(line, event, window, cx)
}),
)
.on_mouse_move(cx.listener(move |editor, event: &MouseMoveEvent, _, cx| {
editor.drag(line, event, cx)
}));
@@ -392,6 +398,27 @@ impl CodeEditor {
self.set_selections(chosen, cx);
}
/// A right press keeps a selection it lands in.
pub(crate) fn secondary_press(
&mut self,
line: usize,
event: &MouseDownEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
window.focus(&self.focus, cx);
let Some(at) = self.offset_in(line, event.position.x, cx) else {
return;
};
let held = (self.selections.iter()).any(|selection| {
let range = selection.range();
!range.is_empty() && (range.start..=range.end).contains(&at)
});
if !held {
self.set_selections(vec![Selection::caret(at)], cx);
}
}
/// A drag over a line stretches the primary selection to where the pointer is.
pub(crate) fn drag(&mut self, line: usize, event: &MouseMoveEvent, cx: &mut Context<Self>) {
if !self.dragging || !event.dragging() {
+20
View File
@@ -343,6 +343,25 @@ impl TextInput {
}
}
/// A right press keeps a selection it lands in.
fn secondary_down(
&mut self,
event: &MouseDownEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.disabled {
return;
}
window.focus(&self.focus, cx);
let at = self.offset_for_point(event.position);
let held =
!self.selection.is_empty() && (self.selection.start..=self.selection.end).contains(&at);
if !held {
self.move_to(at, cx);
}
}
fn mouse_move(&mut self, event: &MouseMoveEvent, _: &mut Window, cx: &mut Context<Self>) {
if self.selecting && event.dragging() {
let at = self.offset_for_point(event.position);
@@ -405,6 +424,7 @@ impl Render for TextInput {
.on_action(cx.listener(Self::submit))
.on_action(cx.listener(Self::show_character_palette))
.on_mouse_down(MouseButton::Left, cx.listener(Self::mouse_down))
.on_mouse_down(MouseButton::Right, cx.listener(Self::secondary_down))
.on_mouse_move(cx.listener(Self::mouse_move))
.on_mouse_up(MouseButton::Left, cx.listener(Self::mouse_up))
.on_mouse_up_out(MouseButton::Left, cx.listener(Self::mouse_up))