Skip to main content

cosmic/widget/
text_context_menu.rs

1// Copyright 2025 System76 <info@system76.com>
2// SPDX-License-Identifier: MPL-2.0
3
4//! Right-click context menu for widgets with selectable text.
5//!
6//! Use [`context_menu_overlay`] from your widget's `overlay()` method
7//! with any widget that implements
8//! [`HasSelectableText`](iced_core::widget::text::HasSelectableText)
9//! to get a context menu with Copy, Select All, and optionally Cut/Paste.
10//!
11//! Internally uses libcosmic's [`Menu`](crate::widget::menu) system for
12//! proper rendering, hover effects, and positioning.
13//!
14//! On Wayland, [`create_text_context_popup`] can be used instead to show
15//! the context menu as a native popup surface.
16
17pub use iced_core::widget::text::HasSelectableText;
18#[cfg(wayland_platform)]
19use iced_core::window;
20#[cfg(wayland_platform)]
21use iced_runtime::platform_specific::wayland::CornerRadius;
22
23#[cfg(wayland_platform)]
24use crate::surface::action::LiveSettings;
25use crate::widget::RcElementWrapper;
26#[cfg(wayland_platform)]
27use crate::widget::menu::StyleSheet;
28use crate::widget::menu::{
29    self, CloseCondition, ItemHeight, ItemWidth, Menu, MenuBarState, PathHighlight, menu_roots_diff,
30};
31use crate::{theme, widget};
32
33use iced_core::layout::Limits;
34use iced_core::widget::Tree;
35use iced_core::{
36    Clipboard, Layout, Point, Rectangle, Shell, Size, Vector, clipboard, mouse, overlay, renderer,
37};
38use iced_widget::core::event;
39use std::borrow::Cow;
40use std::sync::{Arc, Mutex};
41
42/// Shared state for communicating deferred context menu actions
43/// from a Wayland popup back to the owning text widget.
44pub(crate) type PendingAction = Arc<Mutex<Option<TextCtxAction>>>;
45
46/// Creates a new [`PendingAction`] for use with popup-based context menus.
47pub(crate) fn pending_action() -> PendingAction {
48    Arc::new(Mutex::new(None))
49}
50
51/// Takes a pending action if one was set by a popup menu, and returns it.
52pub(crate) fn take_pending_action(pending: &PendingAction) -> Option<TextCtxAction> {
53    pending.lock().ok().and_then(|mut guard| guard.take())
54}
55
56use std::cell::Cell;
57
58thread_local! {
59    static CURRENT_WINDOW_ID: Cell<iced_core::window::Id> = const { Cell::new(iced_core::window::Id::NONE) };
60}
61
62#[cfg(wayland_platform)]
63use iced_runtime::platform_specific::wayland::popup::SctkPopupSettings;
64
65/// A request to create a text context-menu popup surface, queued by a widget
66/// during `update()` and drained by `Cosmic::update()` so the popup goes
67/// through the normal `get_popup()` Task + `surface_views` pipeline.
68#[cfg(wayland_platform)]
69pub(crate) struct PopupRequest {
70    live_settings: LiveSettings,
71    settings: SctkPopupSettings,
72    menu: Menu<'static, TextCtxAction>,
73    selected_text: Option<String>,
74    pending_action: PendingAction,
75}
76
77#[cfg(wayland_platform)]
78thread_local! {
79    static PENDING_POPUP_REQUESTS: std::cell::RefCell<Vec<PopupRequest>> =
80        const { std::cell::RefCell::new(Vec::new()) };
81}
82
83pub(crate) fn set_current_window_id(id: iced_core::window::Id) {
84    CURRENT_WINDOW_ID.set(id);
85}
86
87pub(crate) fn current_window_id() -> iced_core::window::Id {
88    CURRENT_WINDOW_ID.get()
89}
90
91/// Drains all popup requests queued by widgets this frame.
92#[cfg(wayland_platform)]
93pub(crate) fn take_popup_requests() -> Vec<PopupRequest> {
94    PENDING_POPUP_REQUESTS.with(|q| std::mem::take(&mut *q.borrow_mut()))
95}
96
97/// Consumes a [`PopupRequest`], returning the popup settings plus a view
98/// builder. The builder rebuilds the menu element each frame from the
99/// captured content, independent of app state.
100#[cfg(wayland_platform)]
101#[allow(clippy::type_complexity)]
102pub(crate) fn into_popup_view<Message: Clone + 'static>(
103    req: PopupRequest,
104) -> (
105    LiveSettings,
106    SctkPopupSettings,
107    Box<dyn Fn() -> crate::Element<'static, crate::Action<Message>> + Send + Sync>,
108) {
109    let PopupRequest {
110        settings,
111        menu,
112        selected_text,
113        pending_action,
114        live_settings,
115    } = req;
116
117    let view = Box::new(move || {
118        let popup_widget: TextContextMenuPopup<Message> = TextContextMenuPopup {
119            menu: menu.clone(),
120            selected_text: selected_text.clone(),
121            pending_action: pending_action.clone(),
122            _phantom: std::marker::PhantomData,
123        };
124        crate::Element::from(crate::widget::container(popup_widget).center(iced_core::Length::Fill))
125            .map(crate::action::app)
126    });
127
128    (live_settings, settings, view)
129}
130
131#[cfg(wayland_platform)]
132thread_local! {
133    static PENDING_POPUP_DESTROYS: std::cell::RefCell<Vec<iced_core::window::Id>> =
134        const { std::cell::RefCell::new(Vec::new()) };
135}
136
137/// Queues a context-menu popup for teardown.
138///
139/// Mirrors [`create_text_context_popup`]'s request queue: widgets running
140/// inside `update()` can't reach `Cosmic` to issue a Task, so they push the
141/// id here and `Cosmic::update()` drains it into a `destroy_popup` Task that
142/// flows through the normal surface pipeline.
143#[cfg(wayland_platform)]
144fn queue_destroy_popup(id: iced_core::window::Id) {
145    PENDING_POPUP_DESTROYS.with(|q| q.borrow_mut().push(id));
146    wake_runtime();
147}
148
149#[cfg(wayland_platform)]
150static WAKE_TX: std::sync::OnceLock<iced_futures::futures::channel::mpsc::Sender<()>> =
151    std::sync::OnceLock::new();
152
153/// Stable identity for [`wake_subscription`].
154#[cfg(wayland_platform)]
155struct PopupWake;
156
157/// Nudges the runtime so `Cosmic::update()` runs and drains the popup queues.
158#[cfg(wayland_platform)]
159fn wake_runtime() {
160    if let Some(tx) = WAKE_TX.get() {
161        let _ = tx.clone().try_send(());
162    }
163}
164
165/// Subscription that backs [`wake_runtime`]: it owns the receiving end of the
166/// wake channel and re-emits each ping as [`crate::Action::None`]. Add it to
167/// the app's subscriptions (done by `Cosmic::subscription`) so popup creation
168/// and teardown queued from widget `update()` get drained promptly.
169#[cfg(wayland_platform)]
170pub(crate) fn wake_subscription<Message: Send + 'static>()
171-> iced_futures::Subscription<crate::Action<Message>> {
172    use iced_futures::futures::{SinkExt, StreamExt};
173    iced_futures::Subscription::run_with(std::any::TypeId::of::<PopupWake>(), |_| {
174        iced::stream::channel(
175            16,
176            |mut output: iced_futures::futures::channel::mpsc::Sender<crate::Action<Message>>| async move {
177                let (tx, mut rx) = iced_futures::futures::channel::mpsc::channel(16);
178                let _ = WAKE_TX.set(tx);
179                while rx.next().await.is_some() {
180                    let _ = output.send(crate::Action::None).await;
181                }
182            },
183        )
184    })
185}
186
187/// Drains all popup teardown requests queued by widgets this frame.
188///
189/// Called by `Cosmic::update()`, which turns each id into a `destroy_popup()`
190/// Task. Drained before the creation queue so a destroy-then-recreate (a
191/// second right-click reusing the same id) keeps its order.
192#[cfg(wayland_platform)]
193pub(crate) fn take_popup_destroys() -> Vec<iced_core::window::Id> {
194    PENDING_POPUP_DESTROYS.with(|q| std::mem::take(&mut *q.borrow_mut()))
195}
196
197/// Creates a context menu overlay for any widget implementing
198/// [`HasSelectableText`].
199///
200/// Call this from your widget's `overlay()` method. Pass `on_input` for
201/// editable widgets so Cut and Paste can publish text-change messages.
202///
203/// The `menu_bar_state` parameter must be a persistent [`MenuBarState`]
204/// stored in the widget's tree state.
205pub(crate) fn context_menu_overlay<'a, W, Message>(
206    widget: &'a W,
207    tree: &'a mut Tree,
208    on_input: Option<&'a dyn Fn(String) -> Message>,
209    translation: Vector,
210    menu_bar_state: MenuBarState,
211) -> Option<overlay::Element<'a, Message, crate::Theme, crate::Renderer>>
212where
213    W: HasSelectableText + 'a,
214    Message: Clone + 'static,
215{
216    let click_position = widget.context_menu_position(tree)?;
217    let selected_text = widget.selected_text(tree);
218    let is_editable = widget.is_editable();
219
220    let mut menu_roots = build_menu_roots(
221        is_editable,
222        selected_text.is_some(),
223        widget.has_text(tree),
224        widget.clipboard_has_text(tree),
225    );
226    menu_roots.iter_mut().for_each(menu::Tree::set_index);
227
228    let bounds = Rectangle {
229        x: click_position.x,
230        y: click_position.y,
231        width: 240.0,
232        height: 240.0,
233    };
234
235    let item_count = menu_roots[0].children.len();
236    menu_bar_state.inner.with_data_mut(|state| {
237        let stale = state.menu_states.first().is_some_and(|ms| {
238            ms.menu_bounds.child_positions.len() != item_count
239                || (ms.menu_bounds.parent_bounds.x - bounds.x).abs() > 0.5
240                || (ms.menu_bounds.parent_bounds.y - bounds.y).abs() > 0.5
241        });
242        if !state.open || stale {
243            state.menu_states.clear();
244            state.active_root.clear();
245            state.open = true;
246        }
247        menu_roots_diff(&mut menu_roots, &mut state.tree);
248    });
249
250    let menu = Menu {
251        tree: menu_bar_state.clone(),
252        menu_roots: Cow::Owned(menu_roots),
253        bounds_expand: 16,
254        menu_overlays_parent: true,
255        close_condition: CloseCondition {
256            leave: false,
257            click_outside: true,
258            click_inside: true,
259        },
260        item_width: ItemWidth::Uniform(240),
261        item_height: ItemHeight::Dynamic(40),
262        bar_bounds: bounds,
263        main_offset: -(bounds.height as i32),
264        cross_offset: 0,
265        root_bounds_list: vec![bounds],
266        path_highlight: Some(PathHighlight::MenuActive),
267        style: Cow::Owned(theme::menu_bar::MenuBarStyle::Default),
268        position: Point::new(translation.x, translation.y),
269        is_overlay: true,
270        window_id: iced::window::Id::NONE,
271        depth: 0,
272        on_surface_action: None,
273    };
274
275    Some(overlay::Element::new(Box::new(TextMenuOverlay {
276        menu,
277        widget,
278        tree,
279        on_input,
280    })))
281}
282
283#[derive(Clone, Copy, PartialEq, Eq)]
284pub(crate) enum TextCtxAction {
285    Copy,
286    Cut,
287    Paste,
288    SelectAll,
289}
290
291fn build_menu_roots(
292    is_editable: bool,
293    has_selection: bool,
294    has_text: bool,
295    clipboard_has_text: bool,
296) -> Vec<menu::Tree<TextCtxAction>> {
297    let item = |label: &'static str, action: TextCtxAction, enabled: bool| {
298        menu::Tree::from(crate::Element::from(
299            menu::menu_button(vec![widget::text(label).into()])
300                .on_press_maybe(enabled.then_some(action)),
301        ))
302    };
303
304    let mut items = Vec::with_capacity(4);
305    if is_editable {
306        items.push(item("Cut", TextCtxAction::Cut, has_selection));
307    }
308    items.push(item("Copy", TextCtxAction::Copy, has_selection));
309    if is_editable {
310        items.push(item("Paste", TextCtxAction::Paste, clipboard_has_text));
311    }
312    items.push(item("Select All", TextCtxAction::SelectAll, has_text));
313
314    vec![menu::Tree::with_children(
315        RcElementWrapper::new(crate::Element::from(widget::Row::new())),
316        items,
317    )]
318}
319
320struct TextMenuOverlay<'a, W, Message: Clone + 'static> {
321    menu: Menu<'a, TextCtxAction>,
322    widget: &'a W,
323    tree: &'a mut Tree,
324    on_input: Option<&'a dyn Fn(String) -> Message>,
325}
326
327impl<W, Message> overlay::Overlay<Message, crate::Theme, crate::Renderer>
328    for TextMenuOverlay<'_, W, Message>
329where
330    W: HasSelectableText,
331    Message: Clone + 'static,
332{
333    fn layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> iced_core::layout::Node {
334        // Initialise the menu before the first draw so it appears at the click
335        // position immediately
336        let needs_init = self
337            .menu
338            .tree
339            .inner
340            .with_data(|state| state.open && state.menu_states.is_empty());
341
342        if needs_init {
343            let overlay_offset = Point::ORIGIN - self.menu.position;
344            let bar_bounds = self.menu.bar_bounds;
345            let main_offset = self.menu.main_offset as f32;
346            let overlay_cursor = bar_bounds.center();
347
348            let mut init_messages: Vec<TextCtxAction> = Vec::new();
349            let mut init_shell = Shell::new(&mut init_messages);
350            menu::init_root_menu(
351                &mut self.menu,
352                renderer,
353                &mut init_shell,
354                overlay_cursor,
355                bounds,
356                overlay_offset,
357                bar_bounds,
358                main_offset,
359            );
360        }
361
362        self.menu.layout(
363            renderer,
364            Limits::NONE
365                .min_width(bounds.width)
366                .max_width(bounds.width)
367                .min_height(bounds.height)
368                .max_height(bounds.height),
369        )
370    }
371
372    fn draw(
373        &self,
374        renderer: &mut crate::Renderer,
375        theme: &crate::Theme,
376        style: &renderer::Style,
377        layout: Layout<'_>,
378        cursor: mouse::Cursor,
379    ) {
380        self.menu.draw(renderer, theme, style, layout, cursor);
381    }
382
383    fn update(
384        &mut self,
385        event: &event::Event,
386        layout: Layout<'_>,
387        cursor: mouse::Cursor,
388        renderer: &crate::Renderer,
389        clipboard: &mut dyn Clipboard,
390        shell: &mut Shell<'_, Message>,
391    ) {
392        // Right-clicks are not menu interactions. A right-click *on* the menu
393        // (notably the press/release that opened it) is swallowed so it does
394        // not close the menu. A right-click *outside* closes it — clearing the
395        // menu position lets the widget reopen it at the new point on the same
396        // event. Initialization happens in `layout()`.
397        if matches!(
398            event,
399            event::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right))
400                | event::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right))
401        ) {
402            let over_menu = cursor
403                .position()
404                .is_some_and(|p| layout.bounds().contains(p));
405            if !over_menu {
406                self.menu.tree.inner.with_data_mut(|state| {
407                    state.menu_states.clear();
408                    state.active_root.clear();
409                    state.open = false;
410                });
411                self.widget.set_context_menu_position(self.tree, None);
412                shell.request_redraw();
413            }
414            return;
415        }
416
417        let mut local_messages = Vec::new();
418        let mut local_shell = Shell::new(&mut local_messages);
419
420        self.menu
421            .update(event, layout, cursor, renderer, clipboard, &mut local_shell);
422
423        if local_shell.is_event_captured() {
424            shell.capture_event();
425        }
426        shell.request_redraw_at(local_shell.redraw_request());
427        if local_shell.is_layout_invalid() {
428            shell.invalidate_layout();
429        }
430
431        for action in local_messages {
432            match action {
433                TextCtxAction::Copy => {
434                    self.widget.copy_to_clipboard(self.tree, clipboard);
435                }
436                TextCtxAction::Cut => {
437                    self.widget.copy_to_clipboard(self.tree, clipboard);
438                    if let Some(contents) = self.widget.delete_selection(self.tree) {
439                        if let Some(on_input) = self.on_input {
440                            shell.publish((on_input)(contents));
441                        }
442                    }
443                }
444                TextCtxAction::Paste => {
445                    let content: String = clipboard
446                        .read(clipboard::Kind::Standard)
447                        .unwrap_or_default();
448                    if let Some(contents) = self.widget.paste_text(self.tree, &content) {
449                        if let Some(on_input) = self.on_input {
450                            shell.publish((on_input)(contents));
451                        }
452                    }
453                }
454                TextCtxAction::SelectAll => {
455                    self.widget.select_all(self.tree);
456                }
457            }
458            self.widget.set_context_menu_position(self.tree, None);
459            // The menu closes and the selection may have changed.
460            shell.request_redraw();
461        }
462
463        let is_open = self.menu.tree.inner.with_data(|state| state.open);
464        if !is_open {
465            self.widget.set_context_menu_position(self.tree, None);
466        }
467    }
468
469    fn mouse_interaction(
470        &self,
471        layout: Layout<'_>,
472        cursor: mouse::Cursor,
473        _renderer: &crate::Renderer,
474    ) -> mouse::Interaction {
475        if cursor.is_over(layout.bounds()) {
476            mouse::Interaction::Idle
477        } else {
478            mouse::Interaction::None
479        }
480    }
481}
482
483/// Queues a Wayland popup surface containing the text context menu.
484///
485/// Pushes a [`PopupRequest`] onto the request queue; `Cosmic::update()`
486/// drains it and creates the popup through `get_popup()`, so it flows
487/// through the normal Task + `surface_views` pipeline.
488#[cfg(wayland_platform)]
489pub(crate) fn create_text_context_popup(
490    click_position: Point,
491    selected_text: Option<String>,
492    is_editable: bool,
493    has_selection: bool,
494    has_text: bool,
495    clipboard_has_text: bool,
496    menu_bar_state: &MenuBarState,
497    pending_action: &PendingAction,
498    renderer: &crate::Renderer,
499    viewport: &Rectangle,
500    cursor: mouse::Cursor,
501    window_id: window::Id,
502) {
503    use iced_runtime::platform_specific::wayland::popup::{SctkPopupSettings, SctkPositioner};
504
505    if window_id == iced_core::window::Id::NONE {
506        return;
507    }
508
509    let mut menu_roots = build_menu_roots(is_editable, has_selection, has_text, clipboard_has_text);
510    menu_roots.iter_mut().for_each(menu::Tree::set_index);
511
512    let id = menu_bar_state.inner.with_data_mut(|state| {
513        state.menu_states.clear();
514        state.active_root.clear();
515        menu_roots_diff(&mut menu_roots, &mut state.tree);
516        if let Some(id) = state.popup_id.get(&window_id).copied() {
517            queue_destroy_popup(id);
518            state.view_cursor = cursor;
519            id
520        } else {
521            state.open = true;
522            state.view_cursor = cursor;
523            iced::window::Id::unique()
524        }
525    });
526
527    let bounds = Rectangle {
528        x: click_position.x,
529        y: click_position.y,
530        width: 240.0,
531        height: 240.0,
532    };
533
534    let mut popup_menu: Menu<'static, TextCtxAction> = Menu {
535        tree: menu_bar_state.clone(),
536        menu_roots: Cow::Owned(menu_roots),
537        bounds_expand: 16,
538        menu_overlays_parent: true,
539        close_condition: CloseCondition {
540            leave: false,
541            click_outside: true,
542            click_inside: true,
543        },
544        item_width: ItemWidth::Uniform(240),
545        item_height: ItemHeight::Dynamic(40),
546        bar_bounds: bounds,
547        main_offset: -(bounds.height as i32),
548        cross_offset: 0,
549        root_bounds_list: vec![bounds],
550        path_highlight: Some(PathHighlight::MenuActive),
551        style: Cow::Owned(theme::menu_bar::MenuBarStyle::Default),
552        position: Point::new(0., 0.),
553        is_overlay: false,
554        window_id: id,
555        depth: 0,
556        on_surface_action: None,
557    };
558
559    {
560        let mut init_messages: Vec<TextCtxAction> = Vec::new();
561        let mut init_shell = Shell::new(&mut init_messages);
562        menu::init_root_menu(
563            &mut popup_menu,
564            renderer,
565            &mut init_shell,
566            cursor.position().unwrap_or_default(),
567            viewport.size(),
568            Vector::new(0., 0.),
569            bounds,
570            -(bounds.height),
571        );
572    }
573
574    let anchor_rect = menu_bar_state.inner.with_data_mut(|state| {
575        state.popup_id.insert(window_id, id);
576        let pos = cursor.position().unwrap_or_default();
577        iced::Rectangle {
578            x: pos.x as i32,
579            y: pos.y as i32,
580            width: 1,
581            height: 1,
582        }
583    });
584
585    let menu_node = popup_menu.layout(renderer, iced::Limits::NONE.min_width(1.).min_height(1.));
586    let popup_size = menu_node.size();
587
588    let positioner = SctkPositioner {
589        size: Some((
590            popup_size.width.ceil() as u32 + 2,
591            popup_size.height.ceil() as u32 + 2,
592        )),
593        anchor_rect,
594        anchor: cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Anchor::None,
595        gravity: cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight,
596        reactive: true,
597        ..Default::default()
598    };
599
600    // Queue the request. `Cosmic::update()` drains it and creates the popup
601    // through `get_popup()`, so it flows through the normal Task +
602    // `surface_views` pipeline (rendering and teardown included).
603    let settings = SctkPopupSettings {
604        parent: window_id,
605        id,
606        positioner,
607        parent_size: None,
608        grab: true,
609        close_with_children: false,
610        input_zone: None,
611    };
612
613    let t = crate::theme::THEME.lock().unwrap();
614    let styling = t.appearance(&crate::theme::menu_bar::MenuBarStyle::Default, false);
615    drop(t);
616    let rad = styling.menu_border_radius;
617    PENDING_POPUP_REQUESTS.with(|q| {
618        q.borrow_mut().push(PopupRequest {
619            settings,
620            menu: popup_menu,
621            selected_text,
622            pending_action: pending_action.clone(),
623            live_settings: LiveSettings {
624                corners: Some(CornerRadius {
625                    top_left: rad[0] as u32,
626                    top_right: rad[1] as u32,
627                    bottom_left: rad[2] as u32,
628                    bottom_right: rad[3] as u32,
629                }),
630                ..Default::default()
631            },
632        });
633    });
634    wake_runtime();
635}
636
637/// Dismisses this widget's open context-menu popup on an outside click,
638/// touch, or Escape.
639#[cfg(wayland_platform)]
640pub(crate) fn dismiss_popup_on_event(
641    menu_bar_state: &MenuBarState,
642    event: &event::Event,
643    window_id: window::Id,
644) {
645    let is_dismiss = matches!(
646        event,
647        event::Event::Mouse(mouse::Event::ButtonPressed(
648            mouse::Button::Left | mouse::Button::Middle
649        )) | event::Event::Keyboard(iced_core::keyboard::Event::KeyPressed {
650            key: iced_core::keyboard::Key::Named(iced_core::keyboard::key::Named::Escape),
651            ..
652        }) | event::Event::Touch(iced_core::touch::Event::FingerPressed { .. })
653    );
654    if !is_dismiss {
655        return;
656    }
657
658    let popup_id = menu_bar_state
659        .inner
660        .with_data(|state| state.popup_id.get(&window_id).copied());
661    if let Some(popup_id) = popup_id {
662        menu_bar_state.inner.with_data_mut(|state| {
663            state.popup_id.retain(|_, v| *v != popup_id);
664            state.reset();
665        });
666        queue_destroy_popup(popup_id);
667    }
668}
669
670/// Widget that wraps [`Menu`] inside a Wayland popup and intercepts
671/// [`TextCtxAction`] messages for clipboard operations.
672#[derive(Clone)]
673struct TextContextMenuPopup<Message: Clone + 'static> {
674    menu: Menu<'static, TextCtxAction>,
675    selected_text: Option<String>,
676    pending_action: PendingAction,
677    _phantom: std::marker::PhantomData<Message>,
678}
679
680impl<Message: Clone + 'static> iced_core::widget::Widget<Message, crate::Theme, crate::Renderer>
681    for TextContextMenuPopup<Message>
682{
683    fn size(&self) -> Size<iced_core::Length> {
684        use iced_core::widget::Widget;
685        Widget::<TextCtxAction, crate::Theme, crate::Renderer>::size(&self.menu)
686    }
687
688    fn tag(&self) -> iced_core::widget::tree::Tag {
689        use iced_core::widget::Widget;
690        Widget::<TextCtxAction, crate::Theme, crate::Renderer>::tag(&self.menu)
691    }
692
693    fn state(&self) -> iced_core::widget::tree::State {
694        use iced_core::widget::Widget;
695        Widget::<TextCtxAction, crate::Theme, crate::Renderer>::state(&self.menu)
696    }
697
698    fn children(&self) -> Vec<iced_core::widget::Tree> {
699        use iced_core::widget::Widget;
700        Widget::<TextCtxAction, crate::Theme, crate::Renderer>::children(&self.menu)
701    }
702
703    fn diff(&mut self, tree: &mut iced_core::widget::Tree) {
704        use iced_core::widget::Widget;
705        Widget::<TextCtxAction, crate::Theme, crate::Renderer>::diff(&mut self.menu, tree);
706    }
707
708    fn layout(
709        &mut self,
710        tree: &mut iced_core::widget::Tree,
711        renderer: &crate::Renderer,
712        limits: &iced_core::layout::Limits,
713    ) -> iced_core::layout::Node {
714        use iced_core::widget::Widget;
715        Widget::<TextCtxAction, crate::Theme, crate::Renderer>::layout(
716            &mut self.menu,
717            tree,
718            renderer,
719            limits,
720        )
721    }
722
723    fn draw(
724        &self,
725        tree: &iced_core::widget::Tree,
726        renderer: &mut crate::Renderer,
727        theme: &crate::Theme,
728        style: &renderer::Style,
729        layout: Layout<'_>,
730        cursor: mouse::Cursor,
731        viewport: &Rectangle,
732    ) {
733        use iced_core::widget::Widget;
734        Widget::<TextCtxAction, crate::Theme, crate::Renderer>::draw(
735            &self.menu, tree, renderer, theme, style, layout, cursor, viewport,
736        );
737    }
738
739    fn update(
740        &mut self,
741        tree: &mut iced_core::widget::Tree,
742        event: &event::Event,
743        layout: Layout<'_>,
744        cursor: mouse::Cursor,
745        renderer: &crate::Renderer,
746        clipboard: &mut dyn Clipboard,
747        shell: &mut Shell<'_, Message>,
748        viewport: &Rectangle,
749    ) {
750        #[cfg(wayland_platform)]
751        {
752            use iced_core::event::wayland::PopupEvent;
753            let popup_event = match event {
754                event::Event::PlatformSpecific(iced_core::event::PlatformSpecific::Wayland(
755                    iced_core::event::wayland::Event::Popup(e, _, _),
756                )) => Some(e),
757                _ => None,
758            };
759            if matches!(popup_event, Some(PopupEvent::Done | PopupEvent::Unfocused)) {
760                let popup_id = self.menu.window_id;
761                self.menu.tree.inner.with_data_mut(|state| {
762                    state.popup_id.retain(|_, v| *v != popup_id);
763                    state.reset();
764                });
765                // `Done` means the surface was already destroyed by the
766                // compositor; only `Unfocused` needs an explicit destroy.
767                if matches!(popup_event, Some(PopupEvent::Unfocused)) {
768                    queue_destroy_popup(popup_id);
769                }
770                return;
771            }
772        }
773
774        // Escape dismisses the popup. Under the grab, keyboard input is
775        // delivered to the popup surface, so handle it here.
776        #[cfg(wayland_platform)]
777        if matches!(
778            event,
779            event::Event::Keyboard(iced_core::keyboard::Event::KeyPressed {
780                key: iced_core::keyboard::Key::Named(iced_core::keyboard::key::Named::Escape),
781                ..
782            })
783        ) {
784            let popup_id = self.menu.window_id;
785            self.menu.tree.inner.with_data_mut(|state| {
786                state.popup_id.retain(|_, v| *v != popup_id);
787                state.reset();
788            });
789            queue_destroy_popup(popup_id);
790            shell.capture_event();
791            return;
792        }
793
794        let mut local_messages: Vec<TextCtxAction> = Vec::new();
795        let mut local_shell = Shell::new(&mut local_messages);
796
797        {
798            use iced_core::widget::Widget;
799            Widget::<TextCtxAction, crate::Theme, crate::Renderer>::update(
800                &mut self.menu,
801                tree,
802                event,
803                layout,
804                cursor,
805                renderer,
806                clipboard,
807                &mut local_shell,
808                viewport,
809            );
810        }
811
812        if local_shell.is_event_captured() {
813            shell.capture_event();
814        }
815        shell.request_redraw_at(local_shell.redraw_request());
816        if local_shell.is_layout_invalid() {
817            shell.invalidate_layout();
818        }
819
820        for action in local_messages {
821            match action {
822                TextCtxAction::Copy => {
823                    if let Some(ref text) = self.selected_text {
824                        clipboard.write(clipboard::Kind::Standard, text.clone());
825                    }
826                }
827                TextCtxAction::Cut => {
828                    if let Some(ref text) = self.selected_text {
829                        clipboard.write(clipboard::Kind::Standard, text.clone());
830                    }
831                    if let Ok(mut guard) = self.pending_action.lock() {
832                        *guard = Some(TextCtxAction::Cut);
833                    }
834                }
835                TextCtxAction::Paste => {
836                    if let Ok(mut guard) = self.pending_action.lock() {
837                        *guard = Some(TextCtxAction::Paste);
838                    }
839                }
840                TextCtxAction::SelectAll => {
841                    if let Ok(mut guard) = self.pending_action.lock() {
842                        *guard = Some(TextCtxAction::SelectAll);
843                    }
844                }
845            }
846        }
847
848        // Under the popup grab the parent widget receives no events, so the
849        // popup must tear itself down once its menu has closed — whether an
850        // item was chosen (`click_inside`) or the user clicked away
851        // (`click_outside`).
852        #[cfg(wayland_platform)]
853        {
854            let menu_closed = self.menu.tree.inner.with_data(|state| !state.open);
855            if menu_closed {
856                let popup_id = self.menu.window_id;
857                self.menu.tree.inner.with_data_mut(|state| {
858                    state.popup_id.retain(|_, v| *v != popup_id);
859                    state.reset();
860                });
861                queue_destroy_popup(popup_id);
862            }
863        }
864    }
865
866    fn mouse_interaction(
867        &self,
868        tree: &iced_core::widget::Tree,
869        layout: Layout<'_>,
870        cursor: mouse::Cursor,
871        viewport: &Rectangle,
872        renderer: &crate::Renderer,
873    ) -> mouse::Interaction {
874        use iced_core::widget::Widget;
875        Widget::<TextCtxAction, crate::Theme, crate::Renderer>::mouse_interaction(
876            &self.menu, tree, layout, cursor, viewport, renderer,
877        )
878    }
879}
880
881impl<Message: Clone + 'static> From<TextContextMenuPopup<Message>>
882    for crate::Element<'static, Message>
883{
884    fn from(popup: TextContextMenuPopup<Message>) -> Self {
885        Self::new(popup)
886    }
887}