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                        &menu_bar_state,
301                        &pending_action,
302                        renderer,
303                        viewport,
304                        cursor,
305                        self.window_id,
306                    );
307
308                    self.inner
309                        .set_context_menu_position(&mut tree.children[0], None);
310                }
311
312                // Process deferred actions from the popup.
313                {
314                    let wrapper_state = tree.state.downcast_ref::<EditorWrapperState>();
315                    let pending_action = wrapper_state.pending_action.clone();
316                    if let Some(action) =
317                        crate::widget::text_context_menu::take_pending_action(&pending_action)
318                    {
319                        match action {
320                            crate::widget::text_context_menu::TextCtxAction::Copy => {}
321                            crate::widget::text_context_menu::TextCtxAction::Cut => {
322                                self.inner.delete_selection(&mut tree.children[0]);
323                            }
324                            crate::widget::text_context_menu::TextCtxAction::Paste => {
325                                let content: String = clipboard
326                                    .read(iced_core::clipboard::Kind::Standard)
327                                    .unwrap_or_default();
328                                self.inner.paste_text(&mut tree.children[0], &content);
329                            }
330                            crate::widget::text_context_menu::TextCtxAction::SelectAll => {
331                                self.inner.select_all(&mut tree.children[0]);
332                            }
333                        }
334                    }
335                }
336
337                // Dismiss popup on outside click / Escape.
338                let wrapper_state = tree.state.downcast_ref::<EditorWrapperState>();
339                let menu_bar_state = wrapper_state.menu_bar_state.clone();
340                crate::widget::text_context_menu::dismiss_popup_on_event(
341                    &menu_bar_state,
342                    event,
343                    self.window_id,
344                );
345            }
346        } else {
347            ew_mut::<Message>(&mut self.inner).update(
348                tree, event, layout, cursor, renderer, clipboard, shell, viewport,
349            );
350        }
351    }
352
353    fn mouse_interaction(
354        &self,
355        tree: &Tree,
356        layout: Layout<'_>,
357        cursor: mouse::Cursor,
358        viewport: &Rectangle,
359        renderer: &crate::Renderer,
360    ) -> mouse::Interaction {
361        let inner_tree = if self.has_context_menu {
362            &tree.children[0]
363        } else {
364            tree
365        };
366        ew::<Message>(&self.inner).mouse_interaction(inner_tree, layout, cursor, viewport, renderer)
367    }
368
369    fn overlay<'b>(
370        &'b mut self,
371        tree: &'b mut Tree,
372        layout: Layout<'b>,
373        renderer: &crate::Renderer,
374        viewport: &Rectangle,
375        translation: Vector,
376    ) -> Option<overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
377        if self.has_context_menu {
378            if !self.uses_popup_context_menu() {
379                use iced_core::widget::text::HasSelectableText;
380                let inner_tree = &mut tree.children[0];
381                if self.inner.context_menu_position(inner_tree).is_some() {
382                    let menu_bar_state = tree
383                        .state
384                        .downcast_ref::<EditorWrapperState>()
385                        .menu_bar_state
386                        .clone();
387                    let inner_tree = &mut tree.children[0];
388                    return crate::widget::text_context_menu::context_menu_overlay(
389                        &self.inner,
390                        inner_tree,
391                        None,
392                        translation,
393                        menu_bar_state,
394                    );
395                }
396            }
397
398            ew_mut::<Message>(&mut self.inner).overlay(
399                &mut tree.children[0],
400                layout,
401                renderer,
402                viewport,
403                translation,
404            )
405        } else {
406            ew_mut::<Message>(&mut self.inner).overlay(
407                tree,
408                layout,
409                renderer,
410                viewport,
411                translation,
412            )
413        }
414    }
415
416    fn operate(
417        &mut self,
418        tree: &mut Tree,
419        layout: Layout<'_>,
420        renderer: &crate::Renderer,
421        operation: &mut dyn iced_core::widget::Operation,
422    ) {
423        let inner_tree = if self.has_context_menu {
424            &mut tree.children[0]
425        } else {
426            tree
427        };
428        ew_mut::<Message>(&mut self.inner).operate(inner_tree, layout, renderer, operation);
429    }
430
431    #[cfg(feature = "a11y")]
432    fn a11y_nodes(
433        &self,
434        layout: Layout<'_>,
435        state: &Tree,
436        cursor: mouse::Cursor,
437    ) -> iced_accessibility::A11yTree {
438        let inner_state = if self.has_context_menu {
439            &state.children[0]
440        } else {
441            state
442        };
443        ew::<Message>(&self.inner).a11y_nodes(layout, inner_state, cursor)
444    }
445
446    fn id(&self) -> Option<iced_core::widget::Id> {
447        ew::<Message>(&self.inner).id()
448    }
449
450    fn set_id(&mut self, id: iced_core::widget::Id) {
451        ew_mut::<Message>(&mut self.inner).set_id(id);
452    }
453}
454
455impl<'a, Message: Clone + 'static> From<TextEditor<'a, Message>> for crate::Element<'a, Message> {
456    fn from(editor: TextEditor<'a, Message>) -> Self {
457        Self::new(editor)
458    }
459}