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(is_editable, selected_text.is_some());
221    menu_roots.iter_mut().for_each(menu::Tree::set_index);
222
223    let bounds = Rectangle {
224        x: click_position.x,
225        y: click_position.y,
226        width: 240.0,
227        height: 240.0,
228    };
229
230    let item_count = menu_roots[0].children.len();
231    menu_bar_state.inner.with_data_mut(|state| {
232        let stale = state.menu_states.first().is_some_and(|ms| {
233            ms.menu_bounds.child_positions.len() != item_count
234                || (ms.menu_bounds.parent_bounds.x - bounds.x).abs() > 0.5
235                || (ms.menu_bounds.parent_bounds.y - bounds.y).abs() > 0.5
236        });
237        if !state.open || stale {
238            state.menu_states.clear();
239            state.active_root.clear();
240            state.open = true;
241        }
242        menu_roots_diff(&mut menu_roots, &mut state.tree);
243    });
244
245    let menu = Menu {
246        tree: menu_bar_state.clone(),
247        menu_roots: Cow::Owned(menu_roots),
248        bounds_expand: 16,
249        menu_overlays_parent: true,
250        close_condition: CloseCondition {
251            leave: false,
252            click_outside: true,
253            click_inside: true,
254        },
255        item_width: ItemWidth::Uniform(240),
256        item_height: ItemHeight::Dynamic(40),
257        bar_bounds: bounds,
258        main_offset: -(bounds.height as i32),
259        cross_offset: 0,
260        root_bounds_list: vec![bounds],
261        path_highlight: Some(PathHighlight::MenuActive),
262        style: Cow::Owned(theme::menu_bar::MenuBarStyle::Default),
263        position: Point::new(translation.x, translation.y),
264        is_overlay: true,
265        window_id: iced::window::Id::NONE,
266        depth: 0,
267        on_surface_action: None,
268    };
269
270    Some(overlay::Element::new(Box::new(TextMenuOverlay {
271        menu,
272        widget,
273        tree,
274        on_input,
275    })))
276}
277
278#[derive(Clone, Copy, PartialEq, Eq)]
279pub(crate) enum TextCtxAction {
280    Copy,
281    Cut,
282    Paste,
283    SelectAll,
284}
285
286fn build_menu_roots(is_editable: bool, has_selection: bool) -> Vec<menu::Tree<TextCtxAction>> {
287    let mut items = Vec::with_capacity(4);
288
289    if is_editable && has_selection {
290        items.push(menu::Tree::from(crate::Element::from(
291            menu::menu_button(vec![widget::text("Cut").into()]).on_press(TextCtxAction::Cut),
292        )));
293    }
294    if has_selection {
295        items.push(menu::Tree::from(crate::Element::from(
296            menu::menu_button(vec![widget::text("Copy").into()]).on_press(TextCtxAction::Copy),
297        )));
298    }
299    if is_editable {
300        items.push(menu::Tree::from(crate::Element::from(
301            menu::menu_button(vec![widget::text("Paste").into()]).on_press(TextCtxAction::Paste),
302        )));
303    }
304    items.push(menu::Tree::from(crate::Element::from(
305        menu::menu_button(vec![widget::text("Select All").into()])
306            .on_press(TextCtxAction::SelectAll),
307    )));
308
309    vec![menu::Tree::with_children(
310        RcElementWrapper::new(crate::Element::from(widget::Row::new())),
311        items,
312    )]
313}
314
315struct TextMenuOverlay<'a, W, Message: Clone + 'static> {
316    menu: Menu<'a, TextCtxAction>,
317    widget: &'a W,
318    tree: &'a mut Tree,
319    on_input: Option<&'a dyn Fn(String) -> Message>,
320}
321
322impl<W, Message> overlay::Overlay<Message, crate::Theme, crate::Renderer>
323    for TextMenuOverlay<'_, W, Message>
324where
325    W: HasSelectableText,
326    Message: Clone + 'static,
327{
328    fn layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> iced_core::layout::Node {
329        // Initialise the menu before the first draw so it appears at the click
330        // position immediately
331        let needs_init = self
332            .menu
333            .tree
334            .inner
335            .with_data(|state| state.open && state.menu_states.is_empty());
336
337        if needs_init {
338            let overlay_offset = Point::ORIGIN - self.menu.position;
339            let bar_bounds = self.menu.bar_bounds;
340            let main_offset = self.menu.main_offset as f32;
341            let overlay_cursor = bar_bounds.center();
342
343            let mut init_messages: Vec<TextCtxAction> = Vec::new();
344            let mut init_shell = Shell::new(&mut init_messages);
345            menu::init_root_menu(
346                &mut self.menu,
347                renderer,
348                &mut init_shell,
349                overlay_cursor,
350                bounds,
351                overlay_offset,
352                bar_bounds,
353                main_offset,
354            );
355        }
356
357        self.menu.layout(
358            renderer,
359            Limits::NONE
360                .min_width(bounds.width)
361                .max_width(bounds.width)
362                .min_height(bounds.height)
363                .max_height(bounds.height),
364        )
365    }
366
367    fn draw(
368        &self,
369        renderer: &mut crate::Renderer,
370        theme: &crate::Theme,
371        style: &renderer::Style,
372        layout: Layout<'_>,
373        cursor: mouse::Cursor,
374    ) {
375        self.menu.draw(renderer, theme, style, layout, cursor);
376    }
377
378    fn update(
379        &mut self,
380        event: &event::Event,
381        layout: Layout<'_>,
382        cursor: mouse::Cursor,
383        renderer: &crate::Renderer,
384        clipboard: &mut dyn Clipboard,
385        shell: &mut Shell<'_, Message>,
386    ) {
387        // Right-clicks are not menu interactions. A right-click *on* the menu
388        // (notably the press/release that opened it) is swallowed so it does
389        // not close the menu. A right-click *outside* closes it — clearing the
390        // menu position lets the widget reopen it at the new point on the same
391        // event. Initialization happens in `layout()`.
392        if matches!(
393            event,
394            event::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right))
395                | event::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right))
396        ) {
397            let over_menu = cursor
398                .position()
399                .is_some_and(|p| layout.bounds().contains(p));
400            if !over_menu {
401                self.menu.tree.inner.with_data_mut(|state| {
402                    state.menu_states.clear();
403                    state.active_root.clear();
404                    state.open = false;
405                });
406                self.widget.set_context_menu_position(self.tree, None);
407                shell.request_redraw();
408            }
409            return;
410        }
411
412        let mut local_messages = Vec::new();
413        let mut local_shell = Shell::new(&mut local_messages);
414
415        self.menu
416            .update(event, layout, cursor, renderer, clipboard, &mut local_shell);
417
418        if local_shell.is_event_captured() {
419            shell.capture_event();
420        }
421        shell.request_redraw_at(local_shell.redraw_request());
422        if local_shell.is_layout_invalid() {
423            shell.invalidate_layout();
424        }
425
426        for action in local_messages {
427            match action {
428                TextCtxAction::Copy => {
429                    self.widget.copy_to_clipboard(self.tree, clipboard);
430                }
431                TextCtxAction::Cut => {
432                    self.widget.copy_to_clipboard(self.tree, clipboard);
433                    if let Some(contents) = self.widget.delete_selection(self.tree) {
434                        if let Some(on_input) = self.on_input {
435                            shell.publish((on_input)(contents));
436                        }
437                    }
438                }
439                TextCtxAction::Paste => {
440                    let content: String = clipboard
441                        .read(clipboard::Kind::Standard)
442                        .unwrap_or_default();
443                    if let Some(contents) = self.widget.paste_text(self.tree, &content) {
444                        if let Some(on_input) = self.on_input {
445                            shell.publish((on_input)(contents));
446                        }
447                    }
448                }
449                TextCtxAction::SelectAll => {
450                    self.widget.select_all(self.tree);
451                }
452            }
453            self.widget.set_context_menu_position(self.tree, None);
454            // The menu closes and the selection may have changed.
455            shell.request_redraw();
456        }
457
458        let is_open = self.menu.tree.inner.with_data(|state| state.open);
459        if !is_open {
460            self.widget.set_context_menu_position(self.tree, None);
461        }
462    }
463
464    fn mouse_interaction(
465        &self,
466        layout: Layout<'_>,
467        cursor: mouse::Cursor,
468        _renderer: &crate::Renderer,
469    ) -> mouse::Interaction {
470        if cursor.is_over(layout.bounds()) {
471            mouse::Interaction::Idle
472        } else {
473            mouse::Interaction::None
474        }
475    }
476}
477
478/// Queues a Wayland popup surface containing the text context menu.
479///
480/// Pushes a [`PopupRequest`] onto the request queue; `Cosmic::update()`
481/// drains it and creates the popup through `get_popup()`, so it flows
482/// through the normal Task + `surface_views` pipeline.
483#[cfg(wayland_platform)]
484pub(crate) fn create_text_context_popup(
485    click_position: Point,
486    selected_text: Option<String>,
487    is_editable: bool,
488    has_selection: bool,
489    menu_bar_state: &MenuBarState,
490    pending_action: &PendingAction,
491    renderer: &crate::Renderer,
492    viewport: &Rectangle,
493    cursor: mouse::Cursor,
494    window_id: window::Id,
495) {
496    use iced_runtime::platform_specific::wayland::popup::{SctkPopupSettings, SctkPositioner};
497
498    if window_id == iced_core::window::Id::NONE {
499        return;
500    }
501
502    let mut menu_roots = build_menu_roots(is_editable, has_selection);
503    menu_roots.iter_mut().for_each(menu::Tree::set_index);
504
505    let id = menu_bar_state.inner.with_data_mut(|state| {
506        state.menu_states.clear();
507        state.active_root.clear();
508        menu_roots_diff(&mut menu_roots, &mut state.tree);
509        if let Some(id) = state.popup_id.get(&window_id).copied() {
510            queue_destroy_popup(id);
511            state.view_cursor = cursor;
512            id
513        } else {
514            state.open = true;
515            state.view_cursor = cursor;
516            iced::window::Id::unique()
517        }
518    });
519
520    let bounds = Rectangle {
521        x: click_position.x,
522        y: click_position.y,
523        width: 240.0,
524        height: 240.0,
525    };
526
527    let mut popup_menu: Menu<'static, TextCtxAction> = Menu {
528        tree: menu_bar_state.clone(),
529        menu_roots: Cow::Owned(menu_roots),
530        bounds_expand: 16,
531        menu_overlays_parent: true,
532        close_condition: CloseCondition {
533            leave: false,
534            click_outside: true,
535            click_inside: true,
536        },
537        item_width: ItemWidth::Uniform(240),
538        item_height: ItemHeight::Dynamic(40),
539        bar_bounds: bounds,
540        main_offset: -(bounds.height as i32),
541        cross_offset: 0,
542        root_bounds_list: vec![bounds],
543        path_highlight: Some(PathHighlight::MenuActive),
544        style: Cow::Owned(theme::menu_bar::MenuBarStyle::Default),
545        position: Point::new(0., 0.),
546        is_overlay: false,
547        window_id: id,
548        depth: 0,
549        on_surface_action: None,
550    };
551
552    {
553        let mut init_messages: Vec<TextCtxAction> = Vec::new();
554        let mut init_shell = Shell::new(&mut init_messages);
555        menu::init_root_menu(
556            &mut popup_menu,
557            renderer,
558            &mut init_shell,
559            cursor.position().unwrap_or_default(),
560            viewport.size(),
561            Vector::new(0., 0.),
562            bounds,
563            -(bounds.height),
564        );
565    }
566
567    let anchor_rect = menu_bar_state.inner.with_data_mut(|state| {
568        state.popup_id.insert(window_id, id);
569        let pos = cursor.position().unwrap_or_default();
570        iced::Rectangle {
571            x: pos.x as i32,
572            y: pos.y as i32,
573            width: 1,
574            height: 1,
575        }
576    });
577
578    let menu_node = popup_menu.layout(renderer, iced::Limits::NONE.min_width(1.).min_height(1.));
579    let popup_size = menu_node.size();
580
581    let positioner = SctkPositioner {
582        size: Some((
583            popup_size.width.ceil() as u32 + 2,
584            popup_size.height.ceil() as u32 + 2,
585        )),
586        anchor_rect,
587        anchor: cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Anchor::None,
588        gravity: cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight,
589        reactive: true,
590        ..Default::default()
591    };
592
593    // Queue the request. `Cosmic::update()` drains it and creates the popup
594    // through `get_popup()`, so it flows through the normal Task +
595    // `surface_views` pipeline (rendering and teardown included).
596    let settings = SctkPopupSettings {
597        parent: window_id,
598        id,
599        positioner,
600        parent_size: None,
601        grab: true,
602        close_with_children: false,
603        input_zone: None,
604    };
605
606    let t = crate::theme::THEME.lock().unwrap();
607    let styling = t.appearance(&crate::theme::menu_bar::MenuBarStyle::Default, false);
608    drop(t);
609    let rad = styling.menu_border_radius;
610    PENDING_POPUP_REQUESTS.with(|q| {
611        q.borrow_mut().push(PopupRequest {
612            settings,
613            menu: popup_menu,
614            selected_text,
615            pending_action: pending_action.clone(),
616            live_settings: LiveSettings {
617                corners: Some(CornerRadius {
618                    top_left: rad[0] as u32,
619                    top_right: rad[1] as u32,
620                    bottom_left: rad[2] as u32,
621                    bottom_right: rad[3] as u32,
622                }),
623                ..Default::default()
624            },
625        });
626    });
627    wake_runtime();
628}
629
630/// Dismisses this widget's open context-menu popup on an outside click,
631/// touch, or Escape.
632#[cfg(wayland_platform)]
633pub(crate) fn dismiss_popup_on_event(
634    menu_bar_state: &MenuBarState,
635    event: &event::Event,
636    window_id: window::Id,
637) {
638    let is_dismiss = matches!(
639        event,
640        event::Event::Mouse(mouse::Event::ButtonPressed(
641            mouse::Button::Left | mouse::Button::Middle
642        )) | event::Event::Keyboard(iced_core::keyboard::Event::KeyPressed {
643            key: iced_core::keyboard::Key::Named(iced_core::keyboard::key::Named::Escape),
644            ..
645        }) | event::Event::Touch(iced_core::touch::Event::FingerPressed { .. })
646    );
647    if !is_dismiss {
648        return;
649    }
650
651    let popup_id = menu_bar_state
652        .inner
653        .with_data(|state| state.popup_id.get(&window_id).copied());
654    if let Some(popup_id) = popup_id {
655        menu_bar_state.inner.with_data_mut(|state| {
656            state.popup_id.retain(|_, v| *v != popup_id);
657            state.reset();
658        });
659        queue_destroy_popup(popup_id);
660    }
661}
662
663/// Widget that wraps [`Menu`] inside a Wayland popup and intercepts
664/// [`TextCtxAction`] messages for clipboard operations.
665#[derive(Clone)]
666struct TextContextMenuPopup<Message: Clone + 'static> {
667    menu: Menu<'static, TextCtxAction>,
668    selected_text: Option<String>,
669    pending_action: PendingAction,
670    _phantom: std::marker::PhantomData<Message>,
671}
672
673impl<Message: Clone + 'static> iced_core::widget::Widget<Message, crate::Theme, crate::Renderer>
674    for TextContextMenuPopup<Message>
675{
676    fn size(&self) -> Size<iced_core::Length> {
677        use iced_core::widget::Widget;
678        Widget::<TextCtxAction, crate::Theme, crate::Renderer>::size(&self.menu)
679    }
680
681    fn tag(&self) -> iced_core::widget::tree::Tag {
682        use iced_core::widget::Widget;
683        Widget::<TextCtxAction, crate::Theme, crate::Renderer>::tag(&self.menu)
684    }
685
686    fn state(&self) -> iced_core::widget::tree::State {
687        use iced_core::widget::Widget;
688        Widget::<TextCtxAction, crate::Theme, crate::Renderer>::state(&self.menu)
689    }
690
691    fn children(&self) -> Vec<iced_core::widget::Tree> {
692        use iced_core::widget::Widget;
693        Widget::<TextCtxAction, crate::Theme, crate::Renderer>::children(&self.menu)
694    }
695
696    fn diff(&mut self, tree: &mut iced_core::widget::Tree) {
697        use iced_core::widget::Widget;
698        Widget::<TextCtxAction, crate::Theme, crate::Renderer>::diff(&mut self.menu, tree);
699    }
700
701    fn layout(
702        &mut self,
703        tree: &mut iced_core::widget::Tree,
704        renderer: &crate::Renderer,
705        limits: &iced_core::layout::Limits,
706    ) -> iced_core::layout::Node {
707        use iced_core::widget::Widget;
708        Widget::<TextCtxAction, crate::Theme, crate::Renderer>::layout(
709            &mut self.menu,
710            tree,
711            renderer,
712            limits,
713        )
714    }
715
716    fn draw(
717        &self,
718        tree: &iced_core::widget::Tree,
719        renderer: &mut crate::Renderer,
720        theme: &crate::Theme,
721        style: &renderer::Style,
722        layout: Layout<'_>,
723        cursor: mouse::Cursor,
724        viewport: &Rectangle,
725    ) {
726        use iced_core::widget::Widget;
727        Widget::<TextCtxAction, crate::Theme, crate::Renderer>::draw(
728            &self.menu, tree, renderer, theme, style, layout, cursor, viewport,
729        );
730    }
731
732    fn update(
733        &mut self,
734        tree: &mut iced_core::widget::Tree,
735        event: &event::Event,
736        layout: Layout<'_>,
737        cursor: mouse::Cursor,
738        renderer: &crate::Renderer,
739        clipboard: &mut dyn Clipboard,
740        shell: &mut Shell<'_, Message>,
741        viewport: &Rectangle,
742    ) {
743        #[cfg(wayland_platform)]
744        {
745            use iced_core::event::wayland::PopupEvent;
746            let popup_event = match event {
747                event::Event::PlatformSpecific(iced_core::event::PlatformSpecific::Wayland(
748                    iced_core::event::wayland::Event::Popup(e, _, _),
749                )) => Some(e),
750                _ => None,
751            };
752            if matches!(popup_event, Some(PopupEvent::Done | PopupEvent::Unfocused)) {
753                let popup_id = self.menu.window_id;
754                self.menu.tree.inner.with_data_mut(|state| {
755                    state.popup_id.retain(|_, v| *v != popup_id);
756                    state.reset();
757                });
758                // `Done` means the surface was already destroyed by the
759                // compositor; only `Unfocused` needs an explicit destroy.
760                if matches!(popup_event, Some(PopupEvent::Unfocused)) {
761                    queue_destroy_popup(popup_id);
762                }
763                return;
764            }
765        }
766
767        // Escape dismisses the popup. Under the grab, keyboard input is
768        // delivered to the popup surface, so handle it here.
769        #[cfg(wayland_platform)]
770        if matches!(
771            event,
772            event::Event::Keyboard(iced_core::keyboard::Event::KeyPressed {
773                key: iced_core::keyboard::Key::Named(iced_core::keyboard::key::Named::Escape),
774                ..
775            })
776        ) {
777            let popup_id = self.menu.window_id;
778            self.menu.tree.inner.with_data_mut(|state| {
779                state.popup_id.retain(|_, v| *v != popup_id);
780                state.reset();
781            });
782            queue_destroy_popup(popup_id);
783            shell.capture_event();
784            return;
785        }
786
787        let mut local_messages: Vec<TextCtxAction> = Vec::new();
788        let mut local_shell = Shell::new(&mut local_messages);
789
790        {
791            use iced_core::widget::Widget;
792            Widget::<TextCtxAction, crate::Theme, crate::Renderer>::update(
793                &mut self.menu,
794                tree,
795                event,
796                layout,
797                cursor,
798                renderer,
799                clipboard,
800                &mut local_shell,
801                viewport,
802            );
803        }
804
805        if local_shell.is_event_captured() {
806            shell.capture_event();
807        }
808        shell.request_redraw_at(local_shell.redraw_request());
809        if local_shell.is_layout_invalid() {
810            shell.invalidate_layout();
811        }
812
813        for action in local_messages {
814            match action {
815                TextCtxAction::Copy => {
816                    if let Some(ref text) = self.selected_text {
817                        clipboard.write(clipboard::Kind::Standard, text.clone());
818                    }
819                }
820                TextCtxAction::Cut => {
821                    if let Some(ref text) = self.selected_text {
822                        clipboard.write(clipboard::Kind::Standard, text.clone());
823                    }
824                    if let Ok(mut guard) = self.pending_action.lock() {
825                        *guard = Some(TextCtxAction::Cut);
826                    }
827                }
828                TextCtxAction::Paste => {
829                    if let Ok(mut guard) = self.pending_action.lock() {
830                        *guard = Some(TextCtxAction::Paste);
831                    }
832                }
833                TextCtxAction::SelectAll => {
834                    if let Ok(mut guard) = self.pending_action.lock() {
835                        *guard = Some(TextCtxAction::SelectAll);
836                    }
837                }
838            }
839        }
840
841        // Under the popup grab the parent widget receives no events, so the
842        // popup must tear itself down once its menu has closed — whether an
843        // item was chosen (`click_inside`) or the user clicked away
844        // (`click_outside`).
845        #[cfg(wayland_platform)]
846        {
847            let menu_closed = self.menu.tree.inner.with_data(|state| !state.open);
848            if menu_closed {
849                let popup_id = self.menu.window_id;
850                self.menu.tree.inner.with_data_mut(|state| {
851                    state.popup_id.retain(|_, v| *v != popup_id);
852                    state.reset();
853                });
854                queue_destroy_popup(popup_id);
855            }
856        }
857    }
858
859    fn mouse_interaction(
860        &self,
861        tree: &iced_core::widget::Tree,
862        layout: Layout<'_>,
863        cursor: mouse::Cursor,
864        viewport: &Rectangle,
865        renderer: &crate::Renderer,
866    ) -> mouse::Interaction {
867        use iced_core::widget::Widget;
868        Widget::<TextCtxAction, crate::Theme, crate::Renderer>::mouse_interaction(
869            &self.menu, tree, layout, cursor, viewport, renderer,
870        )
871    }
872}
873
874impl<Message: Clone + 'static> From<TextContextMenuPopup<Message>>
875    for crate::Element<'static, Message>
876{
877    fn from(popup: TextContextMenuPopup<Message>) -> Self {
878        Self::new(popup)
879    }
880}