Skip to main content

cosmic/widget/
text_editor.rs

1// Copyright 2025 System76 <info@system76.com>
2// SPDX-License-Identifier: MPL-2.0
3
4//! A multi-line text editor
5
6pub use iced_widget::text_editor::{
7    Action, Binding, Catalog, Content, Cursor, Edit, Id, KeyPress, Line, LineEnding, Motion,
8    Position, Selection, State, Status, Style, StyleFn,
9};
10
11use crate::widget::menu::MenuBarState;
12
13use iced_core::event::Event;
14use iced_core::text::highlighter;
15use iced_core::widget::Widget;
16use iced_core::widget::tree::{self, Tree};
17use iced_core::{
18    Clipboard, Layout, Length, Rectangle, Shell, Size, Vector, mouse, overlay, renderer, window,
19};
20
21type InnerEditor<'a, Message> =
22    iced_widget::TextEditor<'a, highlighter::PlainText, Message, crate::Theme, crate::Renderer>;
23
24pub struct TextEditor<'a, Message> {
25    inner: InnerEditor<'a, Message>,
26    has_context_menu: bool,
27    window_id: window::Id,
28}
29
30struct EditorWrapperState {
31    menu_bar_state: MenuBarState,
32    pending_action: crate::widget::text_context_menu::PendingAction,
33}
34
35impl<'a, Message: Clone + 'static> TextEditor<'a, Message> {
36    /// Creates a new [`TextEditor`] from the given [`Content`].
37    pub fn new(content: &'a Content<crate::Renderer>) -> Self {
38        Self {
39            inner: iced_widget::text_editor(content),
40            has_context_menu: true,
41            window_id: crate::widget::text_context_menu::current_window_id(),
42        }
43    }
44
45    /// Controls whether the right-click context menu is shown.
46    ///
47    /// The context menu is enabled by default. Pass `false` to disable it.
48    pub fn context_menu(mut self, enabled: bool) -> Self {
49        self.has_context_menu = enabled;
50        self
51    }
52
53    pub fn id(mut self, id: impl Into<iced_core::widget::Id>) -> Self {
54        self.inner = self.inner.id(id);
55        self
56    }
57
58    pub fn placeholder(mut self, placeholder: impl iced_core::text::IntoFragment<'a>) -> Self {
59        self.inner = self.inner.placeholder(placeholder);
60        self
61    }
62
63    pub fn width(mut self, width: impl Into<iced_core::Pixels>) -> Self {
64        self.inner = self.inner.width(width);
65        self
66    }
67
68    pub fn height(mut self, height: impl Into<Length>) -> Self {
69        self.inner = self.inner.height(height);
70        self
71    }
72
73    pub fn min_height(mut self, min_height: impl Into<iced_core::Pixels>) -> Self {
74        self.inner = self.inner.min_height(min_height);
75        self
76    }
77
78    pub fn max_height(mut self, max_height: impl Into<iced_core::Pixels>) -> Self {
79        self.inner = self.inner.max_height(max_height);
80        self
81    }
82
83    pub fn on_action(mut self, on_edit: impl Fn(Action) -> Message + 'a) -> Self {
84        self.inner = self.inner.on_action(on_edit);
85        self
86    }
87
88    pub fn font(
89        mut self,
90        font: impl Into<<crate::Renderer as iced_core::text::Renderer>::Font>,
91    ) -> Self {
92        self.inner = self.inner.font(font);
93        self
94    }
95
96    pub fn size(mut self, size: impl Into<iced_core::Pixels>) -> Self {
97        self.inner = self.inner.size(size);
98        self
99    }
100
101    pub fn line_height(mut self, line_height: impl Into<iced_core::text::LineHeight>) -> Self {
102        self.inner = self.inner.line_height(line_height);
103        self
104    }
105
106    pub fn padding(mut self, padding: impl Into<iced_core::Padding>) -> Self {
107        self.inner = self.inner.padding(padding);
108        self
109    }
110
111    pub fn wrapping(mut self, wrapping: iced_core::text::Wrapping) -> Self {
112        self.inner = self.inner.wrapping(wrapping);
113        self
114    }
115
116    pub fn key_binding(
117        mut self,
118        key_binding: impl Fn(KeyPress) -> Option<Binding<Message>> + 'a,
119    ) -> Self {
120        self.inner = self.inner.key_binding(key_binding);
121        self
122    }
123
124    #[must_use]
125    pub fn style(mut self, style: impl Fn(&crate::Theme, Status) -> Style + 'a) -> Self
126    where
127        <crate::Theme as Catalog>::Class<'a>:
128            From<iced_widget::text_editor::StyleFn<'a, crate::Theme>>,
129    {
130        self.inner = self.inner.style(style);
131        self
132    }
133
134    fn uses_popup_context_menu(&self) -> bool {
135        #[cfg(all(wayland_platform, feature = "winit"))]
136        if matches!(
137            crate::app::cosmic::WINDOWING_SYSTEM.get(),
138            Some(crate::app::cosmic::WindowingSystem::Wayland)
139        ) {
140            return true;
141        }
142        false
143    }
144}
145
146/// Creates a new [`TextEditor`] from the given [`Content`].
147pub fn text_editor<'a, Message: Clone + 'static>(
148    content: &'a Content<crate::Renderer>,
149) -> TextEditor<'a, Message> {
150    TextEditor::new(content)
151}
152
153fn ew<'x, Message>(
154    inner: &'x InnerEditor<'_, Message>,
155) -> &'x dyn Widget<Message, crate::Theme, crate::Renderer> {
156    inner
157}
158
159fn ew_mut<'x, Message>(
160    inner: &'x mut InnerEditor<'_, Message>,
161) -> &'x mut dyn Widget<Message, crate::Theme, crate::Renderer> {
162    inner
163}
164
165impl<'a, Message: Clone + 'static> Widget<Message, crate::Theme, crate::Renderer>
166    for TextEditor<'a, Message>
167{
168    fn tag(&self) -> tree::Tag {
169        if self.has_context_menu {
170            tree::Tag::of::<EditorWrapperState>()
171        } else {
172            ew::<Message>(&self.inner).tag()
173        }
174    }
175
176    fn state(&self) -> tree::State {
177        if self.has_context_menu {
178            tree::State::new(EditorWrapperState {
179                menu_bar_state: MenuBarState::default(),
180                pending_action: crate::widget::text_context_menu::pending_action(),
181            })
182        } else {
183            ew::<Message>(&self.inner).state()
184        }
185    }
186
187    fn children(&self) -> Vec<Tree> {
188        if self.has_context_menu {
189            vec![Tree::new(ew::<Message>(&self.inner))]
190        } else {
191            ew::<Message>(&self.inner).children()
192        }
193    }
194
195    fn diff(&mut self, tree: &mut Tree) {
196        if self.has_context_menu {
197            if let Some(child) = tree.children.first_mut() {
198                child.diff(
199                    &mut self.inner as &mut dyn Widget<Message, crate::Theme, crate::Renderer>,
200                );
201            }
202        } else {
203            ew_mut::<Message>(&mut self.inner).diff(tree);
204        }
205    }
206
207    fn size(&self) -> Size<Length> {
208        ew::<Message>(&self.inner).size()
209    }
210
211    fn layout(
212        &mut self,
213        tree: &mut Tree,
214        renderer: &crate::Renderer,
215        limits: &iced_core::layout::Limits,
216    ) -> iced_core::layout::Node {
217        let inner_tree = if self.has_context_menu {
218            &mut tree.children[0]
219        } else {
220            tree
221        };
222        ew_mut::<Message>(&mut self.inner).layout(inner_tree, renderer, limits)
223    }
224
225    fn draw(
226        &self,
227        tree: &Tree,
228        renderer: &mut crate::Renderer,
229        theme: &crate::Theme,
230        defaults: &renderer::Style,
231        layout: Layout<'_>,
232        cursor: mouse::Cursor,
233        viewport: &Rectangle,
234    ) {
235        let inner_tree = if self.has_context_menu {
236            &tree.children[0]
237        } else {
238            tree
239        };
240        ew::<Message>(&self.inner).draw(
241            inner_tree, renderer, theme, defaults, layout, cursor, viewport,
242        );
243    }
244
245    fn update(
246        &mut self,
247        tree: &mut Tree,
248        event: &Event,
249        layout: Layout<'_>,
250        cursor: mouse::Cursor,
251        renderer: &crate::Renderer,
252        clipboard: &mut dyn Clipboard,
253        shell: &mut Shell<'_, Message>,
254        viewport: &Rectangle,
255    ) {
256        if self.has_context_menu {
257            ew_mut::<Message>(&mut self.inner).update(
258                &mut tree.children[0],
259                event,
260                layout,
261                cursor,
262                renderer,
263                clipboard,
264                shell,
265                viewport,
266            );
267
268            use iced_core::widget::text::HasSelectableText;
269            if self.inner.is_focused(&tree.children[0])
270                && matches!(
271                    event,
272                    Event::Mouse(mouse::Event::ButtonPressed(_))
273                        | Event::Touch(iced_core::touch::Event::FingerPressed { .. })
274                )
275                && cursor.is_over(layout.bounds())
276            {
277                crate::widget::text_input::notify_focus_change();
278            }
279
280            #[cfg(wayland_platform)]
281            if self.uses_popup_context_menu() {
282                if self
283                    .inner
284                    .context_menu_position(&tree.children[0])
285                    .is_some()
286                {
287                    let selected_text = self.inner.selected_text(&tree.children[0]);
288                    let has_selection = selected_text.is_some();
289                    let click_position =
290                        self.inner.context_menu_position(&tree.children[0]).unwrap();
291                    let wrapper_state = tree.state.downcast_ref::<EditorWrapperState>();
292                    let menu_bar_state = wrapper_state.menu_bar_state.clone();
293                    let pending_action = wrapper_state.pending_action.clone();
294
295                    crate::widget::text_context_menu::create_text_context_popup(
296                        click_position,
297                        selected_text,
298                        self.inner.is_editable(),
299                        has_selection,
300                        self.inner.has_text(&tree.children[0]),
301                        self.inner.clipboard_has_text(&tree.children[0]),
302                        &menu_bar_state,
303                        &pending_action,
304                        renderer,
305                        viewport,
306                        cursor,
307                        self.window_id,
308                    );
309
310                    self.inner
311                        .set_context_menu_position(&mut tree.children[0], None);
312                }
313
314                // Process deferred actions from the popup.
315                {
316                    let wrapper_state = tree.state.downcast_ref::<EditorWrapperState>();
317                    let pending_action = wrapper_state.pending_action.clone();
318                    if let Some(action) =
319                        crate::widget::text_context_menu::take_pending_action(&pending_action)
320                    {
321                        match action {
322                            crate::widget::text_context_menu::TextCtxAction::Copy => {}
323                            crate::widget::text_context_menu::TextCtxAction::Cut => {
324                                self.inner.delete_selection(&mut tree.children[0]);
325                            }
326                            crate::widget::text_context_menu::TextCtxAction::Paste => {
327                                let content: String = clipboard
328                                    .read(iced_core::clipboard::Kind::Standard)
329                                    .unwrap_or_default();
330                                self.inner.paste_text(&mut tree.children[0], &content);
331                            }
332                            crate::widget::text_context_menu::TextCtxAction::SelectAll => {
333                                self.inner.select_all(&mut tree.children[0]);
334                            }
335                        }
336                    }
337                }
338
339                // Dismiss popup on outside click / Escape.
340                let wrapper_state = tree.state.downcast_ref::<EditorWrapperState>();
341                let menu_bar_state = wrapper_state.menu_bar_state.clone();
342                crate::widget::text_context_menu::dismiss_popup_on_event(
343                    &menu_bar_state,
344                    event,
345                    self.window_id,
346                );
347            }
348        } else {
349            ew_mut::<Message>(&mut self.inner).update(
350                tree, event, layout, cursor, renderer, clipboard, shell, viewport,
351            );
352        }
353    }
354
355    fn mouse_interaction(
356        &self,
357        tree: &Tree,
358        layout: Layout<'_>,
359        cursor: mouse::Cursor,
360        viewport: &Rectangle,
361        renderer: &crate::Renderer,
362    ) -> mouse::Interaction {
363        let inner_tree = if self.has_context_menu {
364            &tree.children[0]
365        } else {
366            tree
367        };
368        ew::<Message>(&self.inner).mouse_interaction(inner_tree, layout, cursor, viewport, renderer)
369    }
370
371    fn overlay<'b>(
372        &'b mut self,
373        tree: &'b mut Tree,
374        layout: Layout<'b>,
375        renderer: &crate::Renderer,
376        viewport: &Rectangle,
377        translation: Vector,
378    ) -> Option<overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
379        if self.has_context_menu {
380            if !self.uses_popup_context_menu() {
381                use iced_core::widget::text::HasSelectableText;
382                let inner_tree = &mut tree.children[0];
383                if self.inner.context_menu_position(inner_tree).is_some() {
384                    let menu_bar_state = tree
385                        .state
386                        .downcast_ref::<EditorWrapperState>()
387                        .menu_bar_state
388                        .clone();
389                    let inner_tree = &mut tree.children[0];
390                    return crate::widget::text_context_menu::context_menu_overlay(
391                        &self.inner,
392                        inner_tree,
393                        None,
394                        translation,
395                        menu_bar_state,
396                    );
397                }
398            }
399
400            ew_mut::<Message>(&mut self.inner).overlay(
401                &mut tree.children[0],
402                layout,
403                renderer,
404                viewport,
405                translation,
406            )
407        } else {
408            ew_mut::<Message>(&mut self.inner).overlay(
409                tree,
410                layout,
411                renderer,
412                viewport,
413                translation,
414            )
415        }
416    }
417
418    fn operate(
419        &mut self,
420        tree: &mut Tree,
421        layout: Layout<'_>,
422        renderer: &crate::Renderer,
423        operation: &mut dyn iced_core::widget::Operation,
424    ) {
425        let inner_tree = if self.has_context_menu {
426            &mut tree.children[0]
427        } else {
428            tree
429        };
430        ew_mut::<Message>(&mut self.inner).operate(inner_tree, layout, renderer, operation);
431    }
432
433    #[cfg(feature = "a11y")]
434    fn a11y_nodes(
435        &self,
436        layout: Layout<'_>,
437        state: &Tree,
438        cursor: mouse::Cursor,
439    ) -> iced_accessibility::A11yTree {
440        let inner_state = if self.has_context_menu {
441            &state.children[0]
442        } else {
443            state
444        };
445        ew::<Message>(&self.inner).a11y_nodes(layout, inner_state, cursor)
446    }
447
448    fn id(&self) -> Option<iced_core::widget::Id> {
449        ew::<Message>(&self.inner).id()
450    }
451
452    fn set_id(&mut self, id: iced_core::widget::Id) {
453        ew_mut::<Message>(&mut self.inner).set_id(id);
454    }
455}
456
457impl<'a, Message: Clone + 'static> From<TextEditor<'a, Message>> for crate::Element<'a, Message> {
458    fn from(editor: TextEditor<'a, Message>) -> Self {
459        Self::new(editor)
460    }
461}