Skip to main content

floem/views/
text_editor.rs

1use std::rc::Rc;
2
3use floem_editor_core::{buffer::rope_text::RopeTextVal, indent::IndentStyle};
4use floem_reactive::{RwSignal, Scope, SignalUpdate, SignalWith, UpdaterEffect};
5use peniko::Color;
6
7use lapce_xi_rope::Rope;
8
9use crate::{
10    style::{CursorColor, Style},
11    view::ViewId,
12    view::{IntoView, View},
13    views::editor::{
14        Editor,
15        command::CommandExecuted,
16        id::EditorId,
17        keypress::{KeypressKey, default_key_handler},
18        text::{Document, SimpleStyling, Styling},
19        text_document::{OnUpdate, PreCommand, TextDocument},
20        view::editor_container_view,
21    },
22};
23
24use super::editor::{
25    CurrentLineColor, CursorSurroundingLines, IndentGuideColor, IndentStyleProp, Modal,
26    ModalRelativeLine, PhantomColor, PlaceholderColor, PreeditUnderlineColor, RenderWhitespaceProp,
27    ScrollBeyondLastLine, SelectionColor, ShowIndentGuide, SmartTab, VisibleWhitespaceColor,
28    WrapProp,
29    gutter::{DimColor, GutterClass, LeftOfCenterPadding, RightOfCenterPadding},
30    text::{RenderWhitespace, WrapMethod},
31    view::EditorViewClass,
32};
33
34/// A text editor view built on top of [Editor](super::editor::Editor). See [`text_editor`].
35///
36/// Note: this requires that the document underlying it is a [`TextDocument`] for the use of some
37/// logic.
38pub struct TextEditor {
39    id: ViewId,
40    // /// The scope this view was created in, used for creating the final view
41    cx: Scope,
42    editor: Editor,
43}
44
45// Note: this should typically be kept in sync with Lapce's
46// `defaults/keymaps-common.toml`
47//
48/// A text editor view built on top of [Editor](super::editor::Editor). This is the main editor view used in the
49/// [Lapce](https://lap.dev/lapce/) code editor. The default keymap includes the standard editing keys, for using your
50/// own keymap use [`text_editor_keys`].
51///
52/// ## Default Keymaps
53/// ### Basic Editing
54/// Up + ALT => Move Line Up
55/// Down + ALT => Move Line Down
56///
57/// Delete => Delete Forward
58/// Backspace => Delete Backward
59/// Backspace + Shift => Delete Forward
60///
61/// Home => Move to the start of the file
62/// End => Move to the end of the file
63///
64/// PageUp => Scroll up by a page
65/// PageDown => Scroll down by a page
66///
67/// PageUp + CTRL => Scroll up
68/// PageDown + CTRL => Scroll down
69///
70/// Enter => Insert New Line
71/// Tab => Insert Tab
72///
73/// Up + ALT, Up + SHIFT => Duplicate line up
74/// Down + ALT, Down + SHIFT => Duplicate line down
75///
76/// ### Multi Cursor
77/// i + ALT, i + SHIFT => Insert Cursor at the end of the line
78pub fn text_editor(text: impl Into<Rope>) -> TextEditor {
79    let id = ViewId::new();
80    let cx = Scope::current();
81
82    let doc = Rc::new(TextDocument::new(cx, text));
83    let style = Rc::new(SimpleStyling::new());
84    let editor = Editor::new(cx, doc, style, false);
85
86    let editor_sig = cx.create_rw_signal(editor.clone());
87    let child = cx
88        .enter(|| editor_container_view(editor_sig, |_| true, default_key_handler(editor_sig)))
89        .into_view();
90
91    id.set_children([child]);
92
93    TextEditor { id, cx, editor }
94}
95
96/// A text editor view built on top of [Editor](super::editor::Editor) that allows providing your own keymap callback.
97///
98/// See [`text_editor`] for a list of the default keymaps that you will need to handle yourself if using this function.
99pub fn text_editor_keys(
100    text: impl Into<Rope>,
101    handle_key_event: impl Fn(RwSignal<Editor>, &KeypressKey) -> CommandExecuted + 'static,
102) -> TextEditor {
103    let id = ViewId::new();
104    let cx = Scope::current();
105
106    let doc = Rc::new(TextDocument::new(cx, text));
107    let style = Rc::new(SimpleStyling::new());
108    let editor = Editor::new(cx, doc, style, false);
109
110    let editor_sig = cx.create_rw_signal(editor.clone());
111    let child = cx
112        .enter(|| {
113            editor_container_view(
114                editor_sig,
115                |_| true,
116                move |kp| handle_key_event(editor_sig, &kp),
117            )
118        })
119        .into_view();
120
121    id.set_children([child]);
122
123    TextEditor { id, cx, editor }
124}
125
126impl View for TextEditor {
127    fn id(&self) -> ViewId {
128        self.id
129    }
130
131    fn view_style(&self) -> Option<Style> {
132        Some(Style::new().min_width(25).min_height(10))
133    }
134
135    fn debug_name(&self) -> std::borrow::Cow<'static, str> {
136        "Text Editor".into()
137    }
138
139    fn paint(&mut self, _cx: &mut crate::context::PaintCx) {
140        // Clipping is now handled by the box tree and applied automatically
141        // during traversal. Children are painted by the traversal system.
142        // No explicit painting needed.
143    }
144}
145
146/// The custom style elements that are specific to an [Editor].
147pub struct EditorCustomStyle(pub(crate) Style);
148
149impl EditorCustomStyle {
150    /// Sets whether the gutter should be hidden.
151    pub fn hide_gutter(mut self, hide: bool) -> Self {
152        self.0 = self
153            .0
154            .class(GutterClass, |s| s.apply_if(hide, |s| s.hide()));
155        self
156    }
157
158    /// Sets the text accent color of the gutter.
159    ///
160    /// This is the color of the line number for the current line.
161    /// It will default to the current Text Color
162    pub fn gutter_accent_color(mut self, color: Color) -> Self {
163        self.0 = self.0.class(GutterClass, |s| s.color(color));
164        self
165    }
166
167    /// Sets the text dim color of the gutter.
168    ///
169    /// This is the color of the line number for all lines except the current line.
170    /// If this is not specified it will default to the gutter accent color.
171    pub fn gutter_dim_color(mut self, color: Color) -> Self {
172        self.0 = self.0.class(GutterClass, |s| s.set(DimColor, color));
173        self
174    }
175
176    /// Sets the padding to the left of the numbers in the gutter.
177    pub fn gutter_left_padding(mut self, padding: f64) -> Self {
178        self.0 = self
179            .0
180            .class(GutterClass, |s| s.set(LeftOfCenterPadding, padding));
181        self
182    }
183
184    /// Sets the padding to the right of the numbers in the gutter.
185    pub fn gutter_right_padding(mut self, padding: f64) -> Self {
186        self.0 = self
187            .0
188            .class(GutterClass, |s| s.set(RightOfCenterPadding, padding));
189        self
190    }
191
192    /// Sets the background color of the current line in the gutter
193    pub fn gutter_current_color(mut self, color: Color) -> Self {
194        self.0 = self
195            .0
196            .class(GutterClass, |s| s.set(CurrentLineColor, color));
197        self
198    }
199
200    /// Sets the background color to be applied around selected text.
201    pub fn selection_color(mut self, color: Color) -> Self {
202        self.0 = self
203            .0
204            .class(EditorViewClass, |s| s.set(SelectionColor, color));
205        self
206    }
207
208    /// Sets the indent style.
209    pub fn indent_style(mut self, indent_style: IndentStyle) -> Self {
210        self.0 = self
211            .0
212            .class(EditorViewClass, |s| s.set(IndentStyleProp, indent_style));
213        self
214    }
215
216    /// Sets the color of the indent guide.
217    pub fn indent_guide_color(mut self, color: Color) -> Self {
218        self.0 = self
219            .0
220            .class(EditorViewClass, |s| s.set(IndentGuideColor, color));
221        self
222    }
223
224    /// Sets the method for wrapping lines.
225    pub fn wrap_method(mut self, wrap: WrapMethod) -> Self {
226        self.0 = self.0.class(EditorViewClass, |s| s.set(WrapProp, wrap));
227        self
228    }
229
230    /// Sets the color of the cursor.
231    pub fn cursor_color(mut self, cursor: Color) -> Self {
232        self.0 = self
233            .0
234            .class(EditorViewClass, |s| s.set(CursorColor, cursor));
235        self
236    }
237
238    /// Allow scrolling beyond the last line of the document.
239    pub fn scroll_beyond_last_line(mut self, scroll_beyond: bool) -> Self {
240        self.0 = self.0.class(EditorViewClass, |s| {
241            s.set(ScrollBeyondLastLine, scroll_beyond)
242        });
243        self
244    }
245
246    /// Sets the background color of the current line.
247    pub fn current_line_color(mut self, color: Color) -> Self {
248        self.0 = self
249            .0
250            .class(EditorViewClass, |s| s.set(CurrentLineColor, color));
251        self
252    }
253
254    /// Sets the color of visible whitespace characters.
255    pub fn visible_whitespace(mut self, color: Color) -> Self {
256        self.0 = self
257            .0
258            .class(EditorViewClass, |s| s.set(VisibleWhitespaceColor, color));
259        self
260    }
261
262    /// Sets which white space characters should be rendered.
263    pub fn render_white_space(mut self, render_white_space: RenderWhitespace) -> Self {
264        self.0 = self.0.class(EditorViewClass, |s| {
265            s.set(RenderWhitespaceProp, render_white_space)
266        });
267        self
268    }
269
270    /// Set the number of lines to keep visible above and below the cursor.
271    /// Default: `1`
272    pub fn cursor_surrounding_lines(mut self, lines: usize) -> Self {
273        self.0 = self
274            .0
275            .class(EditorViewClass, |s| s.set(CursorSurroundingLines, lines));
276        self
277    }
278
279    /// Sets whether the indent guides should be displayed.
280    pub fn indent_guide(mut self, show: bool) -> Self {
281        self.0 = self
282            .0
283            .class(EditorViewClass, |s| s.set(ShowIndentGuide, show));
284        self
285    }
286
287    /// Sets the editor's mode to modal or non-modal.
288    pub fn modal(mut self, modal: bool) -> Self {
289        self.0 = self.0.class(EditorViewClass, |s| s.set(Modal, modal));
290        self
291    }
292
293    /// Determines if line numbers are relative in modal mode.
294    pub fn modal_relative_line(mut self, modal_relative_line: bool) -> Self {
295        self.0 = self.0.class(EditorViewClass, |s| {
296            s.set(ModalRelativeLine, modal_relative_line)
297        });
298        self
299    }
300
301    /// Enables or disables smart tab behavior, which inserts the indent style detected in the file when the tab key is pressed.
302    pub fn smart_tab(mut self, smart_tab: bool) -> Self {
303        self.0 = self
304            .0
305            .class(EditorViewClass, |s| s.set(SmartTab, smart_tab));
306        self
307    }
308
309    /// Sets the color of phantom text
310    pub fn phantom_color(mut self, color: Color) -> Self {
311        self.0 = self
312            .0
313            .class(EditorViewClass, |s| s.set(PhantomColor, color));
314        self
315    }
316
317    /// Sets the color of the placeholder text.
318    pub fn placeholder_color(mut self, color: Color) -> Self {
319        self.0 = self
320            .0
321            .class(EditorViewClass, |s| s.set(PlaceholderColor, color));
322        self
323    }
324
325    /// Sets the color of the underline for preedit text.
326    pub fn preedit_underline_color(mut self, color: Color) -> Self {
327        self.0 = self
328            .0
329            .class(EditorViewClass, |s| s.set(PreeditUnderlineColor, color));
330        self
331    }
332}
333
334impl TextEditor {
335    /// Sets the custom style properties of the `TextEditor`.
336    pub fn editor_style(
337        self,
338        style: impl Fn(EditorCustomStyle) -> EditorCustomStyle + 'static,
339    ) -> Self {
340        let id = self.id();
341        let view_state = id.state();
342        let offset = view_state.borrow_mut().style.next_offset();
343        let style = UpdaterEffect::new(
344            move || style(EditorCustomStyle(Style::new())),
345            move |style| id.update_style(offset, style.0),
346        );
347        view_state.borrow_mut().style.push(style.0);
348        self
349    }
350
351    /// Return a reference to the underlying [Editor].
352    pub fn editor(&self) -> &Editor {
353        &self.editor
354    }
355
356    /// Allows for creation of a [TextEditor] with an existing [Editor].
357    pub fn with_editor(self, f: impl FnOnce(&Editor)) -> Self {
358        f(&self.editor);
359        self
360    }
361
362    /// Allows for creation of a [TextEditor] with an existing mutable [Editor].
363    pub fn with_editor_mut(mut self, f: impl FnOnce(&mut Editor)) -> Self {
364        f(&mut self.editor);
365        self
366    }
367
368    /// Returns the [EditorId] of the underlying [Editor].
369    pub fn editor_id(&self) -> EditorId {
370        self.editor.id()
371    }
372
373    /// Opens the `TextEditor` with the provided [`Document`].
374    /// You should usually not swap this out without good reason.
375    pub fn with_doc(self, f: impl FnOnce(&dyn Document)) -> Self {
376        self.editor.doc.with_untracked(|doc| {
377            f(doc.as_ref());
378        });
379        self
380    }
381
382    /// Returns a reference to the underlying [Document]. This should usually be a [TextDocument].
383    pub fn doc(&self) -> Rc<dyn Document> {
384        self.editor.doc()
385    }
386
387    /// Retrieves the [Document] and subscribes the current reactive scope to updates.
388    /// Returns `None` if the underlying reactive scope has been disposed.
389    pub fn try_doc(&self) -> Option<Rc<dyn Document>> {
390        self.editor.try_doc()
391    }
392
393    /// Retrieves the [Document] without subscribing to reactive updates.
394    /// Returns `None` if the underlying reactive scope has been disposed.
395    pub fn try_doc_untracked(&self) -> Option<Rc<dyn Document>> {
396        self.editor.try_doc_untracked()
397    }
398
399    /// Try downcasting the document to a [`TextDocument`].
400    /// Returns `None` if the document is not a [`TextDocument`].
401    fn text_doc(&self) -> Option<Rc<TextDocument>> {
402        (self.doc() as Rc<dyn ::std::any::Any>).downcast().ok()
403    }
404
405    // TODO(minor): should this be named `text`? Ideally most users should use the rope text version
406    pub fn rope_text(&self) -> RopeTextVal {
407        self.editor.rope_text()
408    }
409
410    /// Use a different document in the text editor
411    pub fn use_doc(self, doc: Rc<dyn Document>) -> Self {
412        self.editor.update_doc(doc, None);
413        self
414    }
415
416    /// Use the same document as another text editor view.
417    ///
418    /// ```rust,ignore
419    /// let primary = text_editor();
420    /// let secondary = text_editor().share_document(&primary);
421    ///
422    /// stack((
423    ///     primary,
424    ///     secondary,
425    /// ))
426    /// ```
427    ///
428    /// If you wish for it to also share the styling, consider using [`TextEditor::shared_editor`]
429    /// instead.
430    pub fn share_doc(self, other: &TextEditor) -> Self {
431        self.use_doc(other.editor.doc())
432    }
433
434    /// Create a new [`TextEditor`] instance from this instance, sharing the document and styling.
435    ///
436    /// ```rust,ignore
437    /// let primary = text_editor();
438    /// let secondary = primary.shared_editor();
439    /// ```
440    ///
441    /// Also see the [Editor example](https://github.com/lapce/floem/tree/main/examples/editor).
442    pub fn shared_editor(&self) -> TextEditor {
443        let id = ViewId::new();
444
445        let doc = self.editor.doc();
446        let style = self.editor.style();
447        let editor = Editor::new(self.cx, doc, style, false);
448
449        let editor_sig = self.cx.create_rw_signal(editor.clone());
450        let child = self
451            .cx
452            .enter(|| editor_container_view(editor_sig, |_| true, default_key_handler(editor_sig)))
453            .into_view();
454
455        id.set_children([child]);
456
457        TextEditor {
458            id,
459            cx: self.cx,
460            editor,
461        }
462    }
463
464    /// Change the [`Styling`] used for the editor.
465    ///
466    /// ```rust,ignore
467    /// let styling = SimpleStyling::builder()
468    ///     .font_size(12)
469    ///     .weight(Weight::BOLD);
470    /// text_editor().styling(styling);
471    /// ```
472    pub fn styling(self, styling: impl Styling + 'static) -> Self {
473        self.styling_rc(Rc::new(styling))
474    }
475
476    /// Use an `Rc<dyn Styling>` to share between different editors.
477    pub fn styling_rc(self, styling: Rc<dyn Styling>) -> Self {
478        self.editor.update_styling(styling);
479        self
480    }
481
482    /// Set the text editor to read only.
483    /// Equivalent to setting [`Editor::read_only`]
484    /// Default: `false`
485    pub fn read_only(self) -> Self {
486        self.editor.read_only.set(true);
487        self
488    }
489
490    /// Set the placeholder text that is displayed when the document is empty.
491    /// Can span multiple lines.
492    /// This is per-editor, not per-document.
493    /// Equivalent to calling [`TextDocument::add_placeholder`]
494    /// Default: `None`
495    ///
496    /// Note: only works for the default backing [`TextDocument`] doc
497    pub fn placeholder(self, text: impl Into<String>) -> Self {
498        if let Some(doc) = self.text_doc() {
499            doc.add_placeholder(self.editor_id(), text.into());
500        }
501
502        self
503    }
504
505    /// When commands are run on the document, this function is called.
506    /// If it returns [`CommandExecuted::Yes`] then further handlers after it, including the
507    /// default handler, are not executed.
508    ///
509    /// ```rust
510    /// use floem::views::editor::command::{Command, CommandExecuted};
511    /// use floem::views::text_editor::text_editor;
512    /// use floem_editor_core::command::EditCommand;
513    /// text_editor("Hello")
514    ///     .pre_command(|ev| {
515    ///         if matches!(ev.cmd, Command::Edit(EditCommand::Undo)) {
516    ///             // Sorry, no undoing allowed
517    ///             CommandExecuted::Yes
518    ///         } else {
519    ///             CommandExecuted::No
520    ///         }
521    ///     })
522    ///     .pre_command(|_| {
523    ///         // This will never be called if command was an undo
524    ///         CommandExecuted::Yes
525    ///     })
526    ///     .pre_command(|_| {
527    ///         // This will never be called
528    ///         CommandExecuted::No
529    ///     });
530    /// ```
531    ///
532    /// Note that these are specific to each text editor view.
533    ///
534    /// Note: only works for the default backing [`TextDocument`] doc
535    pub fn pre_command(self, f: impl Fn(PreCommand) -> CommandExecuted + 'static) -> Self {
536        if let Some(doc) = self.text_doc() {
537            doc.add_pre_command(self.editor.id(), f);
538        }
539        self
540    }
541
542    /// Listen for deltas applied to the editor.
543    ///
544    /// Useful for anything that has positions based in the editor that can be updated after
545    /// typing, such as syntax highlighting.
546    ///
547    /// Note: only works for the default backing [`TextDocument`] doc
548    pub fn update(self, f: impl Fn(OnUpdate) + 'static) -> Self {
549        if let Some(doc) = self.text_doc() {
550            doc.add_on_update(f);
551        }
552        self
553    }
554}