Skip to main content

cosmic/widget/segmented_button/
widget.rs

1// Copyright 2022 System76 <info@system76.com>
2// SPDX-License-Identifier: MPL-2.0
3
4use super::model::{Entity, Model, Selectable};
5use super::{InsertPosition, ReorderEvent};
6use crate::theme::{SegmentedButton as Style, THEME};
7use crate::widget::dnd_destination::DragId;
8use crate::widget::menu::{
9    self, CloseCondition, ItemHeight, ItemWidth, MenuBarState, PathHighlight, menu_roots_children,
10    menu_roots_diff,
11};
12use crate::widget::{Icon, context_menu, icon};
13use crate::{Element, Renderer};
14use derive_setters::Setters;
15use iced::clipboard::dnd::{
16    self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent, SourceEvent,
17};
18use iced::clipboard::mime::AllowedMimeTypes;
19use iced::touch::Finger;
20use iced::{
21    Alignment, Background, Color, Event, Length, Padding, Rectangle, Size, Task, Vector, alignment,
22    keyboard, mouse, touch, window,
23};
24use iced_core::id::Internal;
25use iced_core::mouse::ScrollDelta;
26use iced_core::text::{self, Ellipsize, LineHeight, Renderer as TextRenderer, Shaping, Wrapping};
27use iced_core::widget::operation::Focusable;
28use iced_core::widget::{self, Tree, operation, tree};
29use iced_core::{
30    Border, Clipboard, Layout, Point, Renderer as IcedRenderer, Shadow, Shell, Text, Widget,
31    layout, renderer,
32};
33use iced_runtime::{Action, task};
34use slotmap::{Key, SecondaryMap};
35use std::borrow::Cow;
36use std::cell::{Cell, LazyCell};
37use std::collections::HashSet;
38use std::collections::hash_map::DefaultHasher;
39use std::hash::{Hash, Hasher};
40use std::marker::PhantomData;
41use std::sync::Arc;
42use std::time::{Duration, Instant};
43
44thread_local! {
45    // Prevents two segmented buttons from being focused at the same time.
46    static LAST_FOCUS_UPDATE: LazyCell<Cell<Instant>> = LazyCell::new(|| Cell::new(Instant::now()));
47}
48
49const TAB_REORDER_LOG_TARGET: &str = "libcosmic::widget::tab_reorder";
50
51/// A command that focuses a segmented item stored in a widget.
52pub fn focus<Message: 'static>(id: Id) -> Task<Message> {
53    task::effect(Action::Widget(Box::new(operation::focusable::focus(id.0))))
54}
55
56pub enum ItemBounds {
57    Button(Entity, Rectangle),
58    Divider(Rectangle, bool),
59}
60
61#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62enum DropSide {
63    Before,
64    After,
65}
66
67impl From<DropSide> for InsertPosition {
68    fn from(side: DropSide) -> Self {
69        match side {
70            DropSide::Before => InsertPosition::Before,
71            DropSide::After => InsertPosition::After,
72        }
73    }
74}
75
76#[derive(Clone, Copy, Debug, PartialEq, Eq)]
77struct DropHint {
78    entity: Entity,
79    side: DropSide,
80}
81
82/// Isolates variant-specific behaviors from [`SegmentedButton`].
83pub trait SegmentedVariant {
84    const VERTICAL: bool;
85
86    /// Get the appearance for this variant of the widget.
87    fn variant_appearance(
88        theme: &crate::Theme,
89        style: &crate::theme::SegmentedButton,
90    ) -> super::Appearance;
91
92    /// Calculates the bounds for visible buttons.
93    fn variant_bounds<'b>(
94        &'b self,
95        state: &'b LocalState,
96        bounds: Rectangle,
97    ) -> Box<dyn Iterator<Item = ItemBounds> + 'b>;
98
99    /// Calculates the layout of this variant.
100    fn variant_layout(
101        &self,
102        state: &mut LocalState,
103        renderer: &crate::Renderer,
104        limits: &layout::Limits,
105    ) -> Size;
106}
107
108/// A conjoined group of items that function together as a button.
109#[derive(Setters)]
110#[must_use]
111pub struct SegmentedButton<'a, Variant, SelectionMode, Message: Clone + 'static>
112where
113    Model<SelectionMode>: Selectable,
114    SelectionMode: Default,
115{
116    /// The model borrowed from the application create this widget.
117    #[setters(skip)]
118    pub(super) model: &'a Model<SelectionMode>,
119    /// iced widget ID
120    pub(super) id: Id,
121    /// The icon used for the close button.
122    pub(super) close_icon: Icon,
123    /// Scrolling switches focus between tabs.
124    pub(super) scrollable_focus: bool,
125    /// Show the close icon only when item is hovered.
126    pub(super) show_close_icon_on_hover: bool,
127    /// Padding of the whole widget.
128    #[setters(into)]
129    pub(super) padding: Padding,
130    /// Whether to place dividers between buttons.
131    pub(super) dividers: bool,
132    /// Alignment of button contents.
133    pub(super) button_alignment: Alignment,
134    /// Padding around a button.
135    pub(super) button_padding: [u16; 4],
136    /// Desired height of a button.
137    pub(super) button_height: u16,
138    /// Spacing between icon and text in button.
139    pub(super) button_spacing: u16,
140    /// Maximum width of a button.
141    pub(super) maximum_button_width: u16,
142    /// Minimum width of a button.
143    pub(super) minimum_button_width: u16,
144    /// Spacing for each indent.
145    pub(super) indent_spacing: u16,
146    /// Desired font for active tabs.
147    pub(super) font_active: crate::font::Font,
148    /// Desired font for hovered tabs.
149    pub(super) font_hovered: crate::font::Font,
150    /// Desired font for inactive tabs.
151    pub(super) font_inactive: crate::font::Font,
152    /// Size of the font.
153    pub(super) font_size: f32,
154    /// Desired width of the widget.
155    pub(super) width: Length,
156    /// Desired height of the widget.
157    pub(super) height: Length,
158    /// Desired spacing between items.
159    pub(super) spacing: u16,
160    /// LineHeight of the font.
161    pub(super) line_height: LineHeight,
162    /// Ellipsize strategy for button text.
163    pub(super) ellipsize: Ellipsize,
164    /// Style to draw the widget in.
165    #[setters(into)]
166    pub(super) style: Style,
167    /// The context menu to display when a context is activated
168    #[setters(skip)]
169    pub(super) context_menu: Option<Vec<menu::Tree<Message>>>,
170    /// Emits the ID of the item that was activated.
171    #[setters(skip)]
172    pub(super) on_activate: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
173    #[setters(skip)]
174    pub(super) on_close: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
175    #[setters(skip)]
176    pub(super) on_context: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
177    #[setters(skip)]
178    pub(super) on_middle_press: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
179    #[setters(skip)]
180    pub(super) on_dnd_drop:
181        Option<Box<dyn Fn(Entity, Vec<u8>, String, DndAction) -> Message + 'static>>,
182    pub(super) mimes: Vec<String>,
183    #[setters(skip)]
184    pub(super) on_dnd_enter: Option<Box<dyn Fn(Entity, Vec<String>) -> Message + 'static>>,
185    #[setters(skip)]
186    pub(super) on_dnd_leave: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
187    #[setters(strip_option)]
188    pub(super) drag_id: Option<DragId>,
189    #[setters(skip)]
190    pub(super) tab_drag: Option<TabDragSource<Message>>,
191    #[setters(skip)]
192    pub(super) on_drop_hint: Option<Box<dyn Fn(Option<(Entity, bool)>) -> Message + 'static>>,
193
194    #[setters(skip)]
195    pub(super) on_reorder: Option<Box<dyn Fn(ReorderEvent) -> Message + 'static>>,
196    #[setters(skip)]
197    window_id: window::Id,
198    #[cfg(wayland_platform)]
199    positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner,
200    #[setters(skip)]
201    pub(crate) on_surface_action:
202        Option<Arc<dyn Fn(crate::surface::Action<Message>) -> Message + Send + Sync + 'static>>,
203
204    /// Defines the implementation of this struct
205    variant: PhantomData<Variant>,
206}
207
208impl<'a, Variant, SelectionMode, Message: Clone + 'static>
209    SegmentedButton<'a, Variant, SelectionMode, Message>
210where
211    Self: SegmentedVariant,
212    Model<SelectionMode>: Selectable,
213    SelectionMode: Default,
214{
215    #[inline]
216    pub fn new(model: &'a Model<SelectionMode>) -> Self {
217        Self {
218            model,
219            id: Id::unique(),
220            close_icon: icon::from_name("window-close-symbolic").size(16).icon(),
221            scrollable_focus: false,
222            show_close_icon_on_hover: false,
223            button_alignment: Alignment::Start,
224            padding: Padding::from(0.0),
225            dividers: false,
226            button_padding: [0, 0, 0, 0],
227            button_height: 32,
228            button_spacing: 0,
229            minimum_button_width: u16::MIN,
230            maximum_button_width: u16::MAX,
231            indent_spacing: 16,
232            font_active: crate::font::semibold(),
233            font_hovered: crate::font::default(),
234            font_inactive: crate::font::default(),
235            font_size: 14.0,
236            height: Length::Shrink,
237            width: Length::Fill,
238            spacing: 0,
239            line_height: LineHeight::default(),
240            ellipsize: Ellipsize::default(),
241            style: Style::default(),
242            context_menu: None,
243            on_activate: None,
244            on_close: None,
245            on_context: None,
246            on_middle_press: None,
247            on_dnd_drop: None,
248            on_dnd_enter: None,
249            on_dnd_leave: None,
250            mimes: Vec::new(),
251            variant: PhantomData,
252            drag_id: None,
253            tab_drag: None,
254            on_drop_hint: None,
255            on_reorder: None,
256            window_id: window::Id::RESERVED,
257            #[cfg(wayland_platform)]
258            positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(),
259            on_surface_action: None,
260        }
261    }
262
263    fn update_entity_paragraph(&mut self, state: &mut LocalState, key: Entity) {
264        if let Some(text) = self.model.text.get(key) {
265            let font = if self.button_is_focused(state, key)
266                || state.show_context == Some(key)
267                || self.model.is_active(key)
268            {
269                self.font_active
270            } else if self.button_is_hovered(state, key) {
271                self.font_hovered
272            } else {
273                self.font_inactive
274            };
275
276            let mut hasher = DefaultHasher::new();
277            text.hash(&mut hasher);
278            font.hash(&mut hasher);
279            let text_hash = hasher.finish();
280
281            if let Some(prev_hash) = state.text_hashes.insert(key, text_hash)
282                && prev_hash == text_hash
283            {
284                return;
285            }
286
287            if let Some(paragraph) = state.paragraphs.get_mut(key) {
288                let text = Text {
289                    content: text.as_ref(),
290                    size: iced::Pixels(self.font_size),
291                    bounds: Size::INFINITE,
292                    font,
293                    align_x: text::Alignment::Left,
294                    align_y: alignment::Vertical::Center,
295                    shaping: Shaping::Advanced,
296                    wrapping: Wrapping::None,
297                    line_height: self.line_height,
298                    ellipsize: self.ellipsize,
299                };
300                paragraph.update(text);
301            } else {
302                let text = Text {
303                    content: text.to_string(),
304                    size: iced::Pixels(self.font_size),
305                    bounds: Size::INFINITE,
306                    font,
307                    align_x: text::Alignment::Left,
308                    align_y: alignment::Vertical::Center,
309                    shaping: Shaping::Advanced,
310                    wrapping: Wrapping::None,
311                    line_height: self.line_height,
312                    ellipsize: self.ellipsize,
313                };
314                state.paragraphs.insert(key, crate::Plain::new(text));
315            }
316        }
317    }
318
319    pub fn context_menu(mut self, context_menu: Option<Vec<menu::Tree<Message>>>) -> Self
320    where
321        Message: Clone + 'static,
322    {
323        self.context_menu = context_menu;
324
325        if let Some(ref mut context_menu) = self.context_menu {
326            context_menu.iter_mut().for_each(menu::Tree::set_index);
327        }
328
329        self
330    }
331
332    /// Emitted when a tab is pressed.
333    pub fn on_activate<T>(mut self, on_activate: T) -> Self
334    where
335        T: Fn(Entity) -> Message + 'static,
336    {
337        self.on_activate = Some(Box::new(on_activate));
338        self
339    }
340
341    /// Emitted when a tab close button is pressed.
342    pub fn on_close<T>(mut self, on_close: T) -> Self
343    where
344        T: Fn(Entity) -> Message + 'static,
345    {
346        self.on_close = Some(Box::new(on_close));
347        self
348    }
349
350    /// Emitted when a button is right-clicked.
351    pub fn on_context<T>(mut self, on_context: T) -> Self
352    where
353        T: Fn(Entity) -> Message + 'static,
354    {
355        self.on_context = Some(Box::new(on_context));
356        self
357    }
358
359    /// Emitted when the middle mouse button is pressed on a button.
360    pub fn on_middle_press<T>(mut self, on_middle_press: T) -> Self
361    where
362        T: Fn(Entity) -> Message + 'static,
363    {
364        self.on_middle_press = Some(Box::new(on_middle_press));
365        self
366    }
367
368    /// Enable drag-and-drop support for tabs using the provided payload builder.
369    pub fn enable_tab_drag(mut self, mime: String) -> Self {
370        self.tab_drag = Some(TabDragSource::new(mime));
371        self
372    }
373
374    /// Receive drop hint updates during drag-and-drop.
375    pub fn on_drop_hint(
376        mut self,
377        callback: impl Fn(Option<(Entity, bool)>) -> Message + 'static,
378    ) -> Self {
379        self.on_drop_hint = Some(Box::new(callback));
380        self
381    }
382
383    /// Emit a message when a tab drag is dropped inside this widget.
384    pub fn on_reorder(mut self, callback: impl Fn(ReorderEvent) -> Message + 'static) -> Self {
385        self.on_reorder = Some(Box::new(callback));
386        self
387    }
388
389    /// Set the pointer distance threshold before a drag is started.
390    pub fn tab_drag_threshold(mut self, threshold: f32) -> Self {
391        if let Some(tab_drag) = self.tab_drag.as_mut() {
392            tab_drag.threshold = threshold.max(1.0);
393        }
394        self
395    }
396
397    fn reorder_event_for_drop(&self, state: &LocalState, target: Entity) -> Option<ReorderEvent> {
398        let dragged = state.dragging_tab?;
399        if dragged == target
400            || !self.model.contains_item(dragged)
401            || !self.model.contains_item(target)
402        {
403            return None;
404        }
405        let position = state
406            .drop_hint
407            .filter(|hint| hint.entity == target)
408            .map(|hint| InsertPosition::from(hint.side))
409            .unwrap_or_else(|| self.default_insert_position(dragged, target));
410        Some(ReorderEvent {
411            dragged,
412            target,
413            position,
414        })
415    }
416
417    fn default_insert_position(&self, dragged: Entity, target: Entity) -> InsertPosition {
418        let len = self.model.len();
419        let target_pos = self
420            .model
421            .position(target)
422            .map(|pos| pos as usize)
423            .unwrap_or(len);
424        let from_pos = self
425            .model
426            .position(dragged)
427            .map(|pos| pos as usize)
428            .unwrap_or(target_pos);
429        if from_pos < target_pos {
430            InsertPosition::After
431        } else {
432            InsertPosition::Before
433        }
434    }
435
436    /// Check if an item is enabled.
437    fn is_enabled(&self, key: Entity) -> bool {
438        self.model.items.get(key).is_some_and(|item| item.enabled)
439    }
440
441    /// Handle the dnd drop event.
442    pub fn on_dnd_drop<D: AllowedMimeTypes>(
443        mut self,
444        dnd_drop_handler: impl Fn(Entity, Option<D>, DndAction) -> Message + 'static,
445    ) -> Self {
446        self.on_dnd_drop = Some(Box::new(move |entity, data, mime, action| {
447            dnd_drop_handler(entity, D::try_from((data, mime)).ok(), action)
448        }));
449        self.mimes = D::allowed().into_owned();
450        self
451    }
452
453    /// Handle the dnd enter event.
454    pub fn on_dnd_enter(
455        mut self,
456        dnd_enter_handler: impl Fn(Entity, Vec<String>) -> Message + 'static,
457    ) -> Self {
458        self.on_dnd_enter = Some(Box::new(dnd_enter_handler));
459        self
460    }
461
462    /// Handle the dnd leave event.
463    pub fn on_dnd_leave(mut self, dnd_leave_handler: impl Fn(Entity) -> Message + 'static) -> Self {
464        self.on_dnd_leave = Some(Box::new(dnd_leave_handler));
465        self
466    }
467
468    /// Item the previous item in the widget.
469    fn focus_previous(&mut self, state: &mut LocalState, shell: &mut Shell<'_, Message>) {
470        match state.focused_item {
471            Item::Tab(entity) => {
472                let mut keys = self.iterate_visible_tabs(state).rev();
473
474                while let Some(key) = keys.next() {
475                    if key == entity {
476                        for key in keys {
477                            // Skip disabled buttons.
478                            if !self.is_enabled(key) {
479                                continue;
480                            }
481
482                            state.focused_item = Item::Tab(key);
483                            shell.capture_event();
484                            return;
485                        }
486
487                        break;
488                    }
489                }
490
491                if self.prev_tab_sensitive(state) {
492                    state.focused_item = Item::PrevButton;
493                    shell.capture_event();
494                    return;
495                }
496            }
497
498            Item::NextButton => {
499                if let Some(last) = self.last_tab(state) {
500                    state.focused_item = Item::Tab(last);
501                    shell.capture_event();
502                    return;
503                }
504            }
505
506            Item::None => {
507                if self.next_tab_sensitive(state) {
508                    state.focused_item = Item::NextButton;
509                    shell.capture_event();
510                    return;
511                } else if let Some(last) = self.last_tab(state) {
512                    state.focused_item = Item::Tab(last);
513                    shell.capture_event();
514                    return;
515                }
516            }
517
518            Item::PrevButton | Item::Set => (),
519        }
520
521        state.focused_item = Item::None;
522    }
523
524    /// Item the next item in the widget.
525    fn focus_next(&mut self, state: &mut LocalState, shell: &mut Shell<'_, Message>) {
526        match state.focused_item {
527            Item::Tab(entity) => {
528                let mut keys = self.iterate_visible_tabs(state);
529                while let Some(key) = keys.next() {
530                    if key == entity {
531                        for key in keys {
532                            // Skip disabled buttons.
533                            if !self.is_enabled(key) {
534                                continue;
535                            }
536
537                            state.focused_item = Item::Tab(key);
538                            shell.capture_event();
539                            return;
540                        }
541
542                        break;
543                    }
544                }
545
546                if self.next_tab_sensitive(state) {
547                    state.focused_item = Item::NextButton;
548                    shell.capture_event();
549                    return;
550                }
551            }
552
553            Item::PrevButton => {
554                if let Some(first) = self.first_tab(state) {
555                    state.focused_item = Item::Tab(first);
556                    shell.capture_event();
557                    return;
558                }
559            }
560
561            Item::None => {
562                if self.prev_tab_sensitive(state) {
563                    state.focused_item = Item::PrevButton;
564                    shell.capture_event();
565                    return;
566                } else if let Some(first) = self.first_tab(state) {
567                    state.focused_item = Item::Tab(first);
568                    shell.capture_event();
569                    return;
570                }
571            }
572
573            Item::NextButton | Item::Set => (),
574        }
575
576        state.focused_item = Item::None;
577    }
578
579    fn iterate_visible_tabs<'b>(
580        &'b self,
581        state: &LocalState,
582    ) -> impl DoubleEndedIterator<Item = Entity> + 'b {
583        self.model
584            .order
585            .iter()
586            .copied()
587            .skip(state.buttons_offset)
588            .take(state.buttons_visible)
589    }
590
591    fn first_tab(&self, state: &LocalState) -> Option<Entity> {
592        self.model.order.get(state.buttons_offset).copied()
593    }
594
595    fn last_tab(&self, state: &LocalState) -> Option<Entity> {
596        self.model
597            .order
598            .get(state.buttons_offset + state.buttons_visible)
599            .copied()
600    }
601
602    #[allow(clippy::unused_self)]
603    fn prev_tab_sensitive(&self, state: &LocalState) -> bool {
604        state.buttons_offset > 0
605    }
606
607    fn next_tab_sensitive(&self, state: &LocalState) -> bool {
608        state.buttons_offset < self.model.order.len() - state.buttons_visible
609    }
610
611    pub(super) fn button_dimensions(
612        &self,
613        state: &mut LocalState,
614        font: crate::font::Font,
615        button: Entity,
616    ) -> (f32, f32) {
617        let mut width = 0.0f32;
618        let mut icon_spacing = 0.0f32;
619
620        // Add text to measurement if text was given.
621        if let Some((text, entry)) = self
622            .model
623            .text
624            .get(button)
625            .zip(state.paragraphs.entry(button))
626            && !text.is_empty()
627        {
628            icon_spacing = f32::from(self.button_spacing);
629            let paragraph = entry.or_insert_with(|| {
630                crate::Plain::new(Text {
631                    content: text.to_string(), // TODO should we just use String at this point?
632                    size: iced::Pixels(self.font_size),
633                    bounds: Size::INFINITE,
634                    font,
635                    align_x: text::Alignment::Left,
636                    align_y: alignment::Vertical::Center,
637                    shaping: Shaping::Advanced,
638                    wrapping: Wrapping::default(),
639                    ellipsize: self.ellipsize,
640                    line_height: self.line_height,
641                })
642            });
643
644            let size = paragraph.min_bounds();
645            width += size.width;
646        }
647
648        // Add indent to measurement if found.
649        if let Some(indent) = self.model.indent(button) {
650            width = f32::from(indent).mul_add(f32::from(self.indent_spacing), width);
651        }
652
653        // Add icon to measurement if icon was given.
654        if let Some(icon) = self.model.icon(button) {
655            width += f32::from(icon.size) + icon_spacing;
656        } else if self.model.is_active(button) {
657            // Add selection icon measurements when widget is a selection widget.
658            if let crate::theme::SegmentedButton::Control = self.style {
659                width += 16.0 + icon_spacing;
660            }
661        }
662
663        // Add close button to measurement if found.
664        if self.model.is_closable(button) {
665            width += f32::from(self.close_icon.size) + f32::from(self.button_spacing);
666        }
667
668        // Add button padding to the max size found
669        width += f32::from(self.button_padding[0]) + f32::from(self.button_padding[2]);
670        width = width.min(f32::from(self.maximum_button_width));
671
672        (width, f32::from(self.button_height))
673    }
674
675    /// Resizes paragraph bounds based on the actual available button width so that
676    /// text ellipsis can take effect. Call this after `variant_layout` has populated
677    /// `state.internal_layout` with final button sizes.
678    pub(super) fn resize_paragraphs(&self, state: &mut LocalState, available_width: f32) {
679        if matches!(self.ellipsize, Ellipsize::None) {
680            return;
681        }
682
683        for (nth, key) in self.model.order.iter().copied().enumerate() {
684            if self.model.text(key).is_some_and(|text| !text.is_empty()) {
685                let mut non_text_width =
686                    f32::from(self.button_padding[0]) + f32::from(self.button_padding[2]);
687
688                if let Some(icon) = self.model.icon(key) {
689                    non_text_width += f32::from(icon.size) + f32::from(self.button_spacing);
690                } else if self.model.is_active(key) {
691                    if let crate::theme::SegmentedButton::Control = self.style {
692                        non_text_width += 16.0 + f32::from(self.button_spacing);
693                    }
694                }
695
696                if self.model.is_closable(key) {
697                    non_text_width +=
698                        f32::from(self.close_icon.size) + f32::from(self.button_spacing);
699                }
700
701                let text_width = (available_width - non_text_width).max(0.0);
702
703                if let Some(paragraph) = state.paragraphs.get_mut(key) {
704                    paragraph.resize(Size::new(text_width, f32::INFINITY));
705
706                    // Update internal_layout actual content width so that
707                    // button_alignment centering uses the ellipsized size.
708                    let content_width = paragraph.min_bounds().width + non_text_width
709                        - f32::from(self.button_padding[0])
710                        - f32::from(self.button_padding[2]);
711                    if let Some(entry) = state.internal_layout.get_mut(nth) {
712                        entry.1.width = content_width;
713                    }
714                }
715            }
716        }
717    }
718
719    pub(super) fn max_button_dimensions(
720        &self,
721        state: &mut LocalState,
722        renderer: &Renderer,
723    ) -> (f32, f32) {
724        let mut width = 0.0f32;
725        let mut height = 0.0f32;
726        let font = renderer.default_font();
727
728        for key in self.model.order.iter().copied() {
729            let (button_width, button_height) = self.button_dimensions(state, font, key);
730
731            state.internal_layout.push((
732                Size::new(button_width, button_height),
733                Size::new(
734                    button_width
735                        - f32::from(self.button_padding[0])
736                        - f32::from(self.button_padding[2]),
737                    button_height,
738                ),
739            ));
740
741            height = height.max(button_height);
742            width = width.max(button_width);
743        }
744
745        for (size, actual) in &mut state.internal_layout {
746            size.height = height;
747            actual.height = height;
748        }
749
750        (width, height)
751    }
752
753    fn button_is_focused(&self, state: &LocalState, key: Entity) -> bool {
754        state.focused.is_some()
755            && self.on_activate.is_some()
756            && Item::Tab(key) == state.focused_item
757    }
758
759    fn button_is_hovered(&self, state: &LocalState, key: Entity) -> bool {
760        self.on_activate.is_some() && state.hovered == Item::Tab(key)
761            || state
762                .dnd_state
763                .drag_offer
764                .as_ref()
765                .is_some_and(|id| id.data.is_some_and(|d| d == key))
766    }
767
768    fn button_is_pressed(&self, state: &LocalState, key: Entity) -> bool {
769        state.pressed_item == Some(Item::Tab(key))
770    }
771
772    fn emit_drop_hint(&self, shell: &mut Shell<'_, Message>, hint: Option<DropHint>) {
773        if let Some(on_hint) = self.on_drop_hint.as_ref() {
774            let mapped = hint.map(|hint| (hint.entity, matches!(hint.side, DropSide::After)));
775            shell.publish(on_hint(mapped));
776        }
777    }
778
779    fn drop_hint_for_position(
780        &self,
781        state: &LocalState,
782        bounds: Rectangle,
783        cursor: Point,
784    ) -> Option<DropHint> {
785        let _ = state.dragging_tab?;
786
787        self.variant_bounds(state, bounds)
788            .filter_map(|item| match item {
789                ItemBounds::Button(entity, rect) if rect.contains(cursor) => Some((entity, rect)),
790                _ => None,
791            })
792            .map(|(entity, rect)| {
793                let before = if Self::VERTICAL {
794                    cursor.y < rect.center_y()
795                } else {
796                    cursor.x < rect.center_x()
797                };
798                DropHint {
799                    entity,
800                    side: if before {
801                        DropSide::Before
802                    } else {
803                        DropSide::After
804                    },
805                }
806            })
807            .next()
808    }
809
810    fn start_tab_drag(
811        &self,
812        state: &mut LocalState,
813        entity: Entity,
814        bounds: Rectangle,
815        cursor: Point,
816        clipboard: &mut dyn Clipboard,
817    ) -> bool {
818        let Some(tab_drag) = self.tab_drag.as_ref() else {
819            return false;
820        };
821
822        log::trace!(
823            target: TAB_REORDER_LOG_TARGET,
824            "start_tab_drag requested entity={:?} cursor=({:.2},{:.2}) bounds=({:.2},{:.2},{:.2},{:.2}) threshold={}",
825            entity,
826            cursor.x,
827            cursor.y,
828            bounds.x,
829            bounds.y,
830            bounds.width,
831            bounds.height,
832            tab_drag.threshold
833        );
834
835        let data_len = 0;
836
837        iced_core::clipboard::start_dnd::<crate::Theme, crate::Renderer>(
838            clipboard,
839            false,
840            Some(iced_core::clipboard::DndSource::Widget(self.id.0.clone())),
841            None,
842            Box::new(SimpleDragData::new(tab_drag.mime.clone(), vec![1])),
843            DndAction::Move,
844        );
845        log::trace!(
846            target: TAB_REORDER_LOG_TARGET,
847            "tab drag started entity={:?} mime={} bytes={}",
848            entity,
849            tab_drag.mime,
850            data_len
851        );
852
853        state.dragging_tab = Some(entity);
854        state.tab_drag_candidate = None;
855        state.pressed_item = None;
856        true
857    }
858
859    /// Returns the drag id of the destination.
860    ///
861    /// # Panics
862    /// Panics if the destination has been assigned a Set id, which is invalid.
863    #[must_use]
864    pub fn get_drag_id(&self) -> u128 {
865        self.drag_id.map_or_else(
866            || {
867                u128::from(match &self.id.0.0 {
868                    Internal::Unique(id) | Internal::Custom(id, _) => *id,
869                    Internal::Set(_) => panic!("Invalid Id assigned to dnd destination."),
870                })
871            },
872            |id| id.0,
873        )
874    }
875
876    #[cfg(wayland_platform)]
877    pub fn with_positioner(
878        mut self,
879        positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner,
880    ) -> Self {
881        self.positioner = positioner;
882        self
883    }
884
885    #[must_use]
886    pub fn window_id(mut self, id: window::Id) -> Self {
887        self.window_id = id;
888        self
889    }
890
891    #[must_use]
892    pub fn window_id_maybe(mut self, id: Option<window::Id>) -> Self {
893        if let Some(id) = id {
894            self.window_id = id;
895        }
896        self
897    }
898
899    #[must_use]
900    pub fn on_surface_action(
901        mut self,
902        handler: impl Fn(crate::surface::Action<Message>) -> Message + Send + Sync + 'static,
903    ) -> Self {
904        self.on_surface_action = Some(Arc::new(handler));
905        self
906    }
907
908    #[cfg(wayland_platform)]
909    #[allow(clippy::too_many_lines)]
910    fn create_popup<'b>(
911        &mut self,
912        layout: Layout<'b>,
913        view_cursor: mouse::Cursor,
914        renderer: &'b Renderer,
915        shell: &'b mut Shell<'_, Message>,
916        viewport: &'b Rectangle,
917        tree: &'b mut Tree,
918    ) {
919        let state = tree.state.downcast_mut::<LocalState>();
920        let my_state = state.menu_state.clone();
921
922        if self.window_id != window::Id::NONE {
923            use crate::surface::action::{LiveSettings, destroy_popup};
924            use crate::widget::menu::StyleSheet;
925            use iced_runtime::platform_specific::wayland::CornerRadius;
926            use iced_runtime::platform_specific::wayland::popup::{
927                SctkPopupSettings, SctkPositioner,
928            };
929
930            let Some(surface_action) = self.on_surface_action.as_ref() else {
931                return;
932            };
933
934            let id = my_state.inner.with_data_mut(|state| {
935                if let Some(id) = state.popup_id.get(&self.window_id).copied() {
936                    // close existing popups
937                    state.menu_states.clear();
938                    state.active_root.clear();
939                    shell.publish(surface_action(destroy_popup(id)));
940                    state.view_cursor = view_cursor;
941                }
942                // A fresh id per popup, so the old popup's Done cannot be mistaken for the new one's
943                window::Id::unique()
944            });
945            let Some(entity) = state.show_context else {
946                return;
947            };
948
949            let Some((mut bounds, i)) = self
950                .variant_bounds(state, layout.bounds())
951                .filter_map(|item| match item {
952                    ItemBounds::Button(entity, bounds) => Some((bounds, entity)),
953                    _ => None,
954                })
955                .enumerate()
956                .find_map(|(i, (bounds, e))| if e == entity { Some((bounds, i)) } else { None })
957            else {
958                return;
959            };
960
961            assert!(
962                self.context_menu
963                    .as_ref()
964                    .is_none_or(|m| m[0].children.len() == self.model.len()),
965                "model length must match the number of context menus"
966            );
967            let menu = self
968                .context_menu
969                .as_mut()
970                .map(|m| m[0].children[i].clone())
971                .unwrap();
972
973            bounds.x = state.context_cursor.x;
974            bounds.y = state.context_cursor.y;
975
976            let mut popup_menu: menu::Menu<'static, _> = menu::Menu {
977                tree: my_state.clone(),
978                menu_roots: std::borrow::Cow::Owned(vec![menu]),
979                bounds_expand: 0,
980                menu_overlays_parent: false,
981                close_condition: CloseCondition {
982                    leave: false,
983                    click_outside: true,
984                    click_inside: true,
985                },
986                item_width: ItemWidth::Uniform(240),
987                item_height: ItemHeight::Dynamic(40),
988                bar_bounds: bounds,
989                main_offset: 0,
990                cross_offset: 0,
991                root_bounds_list: vec![bounds],
992                path_highlight: Some(PathHighlight::MenuActive),
993                style: std::borrow::Cow::Borrowed(&crate::theme::menu_bar::MenuBarStyle::Default),
994                position: Point::new(0., 0.),
995                is_overlay: false,
996                window_id: id,
997                depth: 0,
998                on_surface_action: self.on_surface_action.clone(),
999            };
1000
1001            menu::init_root_menu(
1002                &mut popup_menu,
1003                renderer,
1004                shell,
1005                view_cursor.position().unwrap(),
1006                viewport.size(),
1007                Vector::new(0., 0.),
1008                bounds,
1009                0., // TODO offset?
1010            );
1011            let (anchor_rect, gravity) = my_state.inner.with_data_mut(|state| {
1012                state.popup_id.insert(self.window_id, id);
1013                (state
1014                    .menu_states
1015                    .iter()
1016                    .find(|s| s.index.is_none())
1017                    .map(|s| s.menu_bounds.parent_bounds)
1018                    .map_or_else(
1019                        || {
1020                            let bounds = layout.bounds();
1021                            Rectangle {
1022                                x: bounds.x as i32,
1023                                y: bounds.y as i32,
1024                                width: 1,
1025                                height: 1,
1026                            }
1027                        },
1028                        |r| Rectangle {
1029                            x: r.x as i32,
1030                            y: r.y as i32,
1031                            width: 1,
1032                            height: 1,
1033                        },
1034                    ), match (state.horizontal_direction, state.vertical_direction) {
1035                        (menu::Direction::Positive, menu::Direction::Positive) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight,
1036                        (menu::Direction::Positive, menu::Direction::Negative) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::TopRight,
1037                        (menu::Direction::Negative, menu::Direction::Positive) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomLeft,
1038                        (menu::Direction::Negative, menu::Direction::Negative) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::TopLeft,
1039                    })
1040            });
1041
1042            let menu_node =
1043                popup_menu.layout(renderer, layout::Limits::NONE.min_width(1.).min_height(1.));
1044            let popup_size = menu_node.size();
1045            let positioner = SctkPositioner {
1046                size: Some((
1047                    popup_size.width.ceil() as u32 + 2,
1048                    popup_size.height.ceil() as u32 + 2,
1049                )),
1050                anchor_rect,
1051                anchor:
1052                    cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Anchor::BottomLeft,
1053                gravity,
1054                reactive: true,
1055                ..Default::default()
1056            };
1057            let parent = self.window_id;
1058
1059            let t = THEME.lock().unwrap();
1060            let styling = t.appearance(&crate::theme::menu_bar::MenuBarStyle::Default, false);
1061            drop(t);
1062            let rad = styling.menu_border_radius;
1063
1064            /// Used to create a popup message from within a widget.
1065            #[cfg(wayland_platform)]
1066            #[must_use]
1067            pub fn simple_popup<Message: 'static>(
1068                live_settings: impl Fn() -> LiveSettings + Send + Sync + 'static,
1069                settings: impl Fn()
1070                    -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings
1071                + Send
1072                + Sync
1073                + 'static,
1074                view: Option<impl Fn() -> crate::Element<'static, Message> + Send + Sync + 'static>,
1075            ) -> crate::surface::Action<Message> {
1076                use std::any::Any;
1077
1078                let boxed: Box<
1079                    dyn Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings
1080                        + Send
1081                        + Sync
1082                        + 'static,
1083                > = Box::new(settings);
1084                let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed);
1085
1086                let boxed_live: Box<dyn Fn() -> LiveSettings + Send + Sync + 'static> =
1087                    Box::new(live_settings);
1088                let boxed_live: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed_live);
1089
1090                crate::surface::Action::Popup(
1091                    Arc::new(boxed),
1092                    Arc::new(boxed_live),
1093                    view.map(|view| {
1094                        Arc::new(move || view().map(crate::Action::App))
1095                            as crate::surface::View<Message>
1096                    }),
1097                )
1098            }
1099            shell.publish((surface_action)(simple_popup(
1100                move || LiveSettings {
1101                    corners: Some(CornerRadius {
1102                        top_left: rad[0] as u32,
1103                        top_right: rad[1] as u32,
1104                        bottom_left: rad[2] as u32,
1105                        bottom_right: rad[3] as u32,
1106                    }),
1107                    ..Default::default()
1108                },
1109                move || SctkPopupSettings {
1110                    parent,
1111                    id,
1112                    positioner: positioner.clone(),
1113                    parent_size: None,
1114                    grab: true,
1115                    close_with_children: false,
1116                    input_zone: None,
1117                },
1118                Some(move || {
1119                    Element::from(crate::widget::container(popup_menu.clone()).center(Length::Fill))
1120                }),
1121            )));
1122        }
1123    }
1124}
1125
1126impl<Variant, SelectionMode, Message> Widget<Message, crate::Theme, Renderer>
1127    for SegmentedButton<'_, Variant, SelectionMode, Message>
1128where
1129    Self: SegmentedVariant,
1130    Model<SelectionMode>: Selectable,
1131    SelectionMode: Default,
1132    Message: 'static + Clone,
1133{
1134    fn id(&self) -> Option<widget::Id> {
1135        Some(self.id.0.clone())
1136    }
1137
1138    fn set_id(&mut self, id: widget::Id) {
1139        self.id = Id(id);
1140    }
1141
1142    fn children(&self) -> Vec<Tree> {
1143        let mut children = Vec::new();
1144
1145        // Assign the context menu's elements as this widget's children.
1146        if let Some(ref context_menu) = self.context_menu {
1147            let mut tree = Tree::empty();
1148            tree.state = tree::State::new(MenuBarState::default());
1149            tree.children = menu_roots_children(context_menu);
1150            children.push(tree);
1151        }
1152
1153        children
1154    }
1155
1156    fn tag(&self) -> tree::Tag {
1157        tree::Tag::of::<LocalState>()
1158    }
1159
1160    fn state(&self) -> tree::State {
1161        #[allow(clippy::default_trait_access)]
1162        tree::State::new(LocalState {
1163            menu_state: Default::default(),
1164            paragraphs: SecondaryMap::new(),
1165            text_hashes: SecondaryMap::new(),
1166            buttons_visible: Default::default(),
1167            buttons_offset: Default::default(),
1168            collapsed: Default::default(),
1169            focused: Default::default(),
1170            focused_item: Default::default(),
1171            focused_visible: false,
1172            hovered: Default::default(),
1173            known_length: Default::default(),
1174            middle_clicked: Default::default(),
1175            internal_layout: Default::default(),
1176            context_cursor: Point::default(),
1177            show_context: Default::default(),
1178            wheel_timestamp: Default::default(),
1179            dnd_state: Default::default(),
1180            fingers_pressed: Default::default(),
1181            pressed_item: None,
1182            tab_drag_candidate: None,
1183            dragging_tab: None,
1184            drop_hint: None,
1185            offer_mimes: Vec::new(),
1186        })
1187    }
1188
1189    fn diff(&mut self, tree: &mut Tree) {
1190        let state = tree.state.downcast_mut::<LocalState>();
1191        for key in self.model.order.iter().copied() {
1192            self.update_entity_paragraph(state, key);
1193        }
1194
1195        // Diff the context menu
1196        if let Some(context_menu) = &mut self.context_menu {
1197            state.menu_state.inner.with_data_mut(|inner| {
1198                menu_roots_diff(context_menu, &mut inner.tree);
1199            });
1200        }
1201
1202        // Unfocus if another segmented control was focused.
1203        if let Some(f) = state.focused.as_ref()
1204            && f.updated_at != LAST_FOCUS_UPDATE.with(|f| f.get())
1205        {
1206            state.unfocus();
1207        }
1208    }
1209
1210    fn size(&self) -> Size<Length> {
1211        Size::new(self.width, self.height)
1212    }
1213
1214    fn layout(
1215        &mut self,
1216        tree: &mut Tree,
1217        renderer: &Renderer,
1218        limits: &layout::Limits,
1219    ) -> layout::Node {
1220        let state = tree.state.downcast_mut::<LocalState>();
1221        let limits = limits.shrink(self.padding);
1222        let size = self
1223            .variant_layout(state, renderer, &limits)
1224            .expand(self.padding);
1225        layout::Node::new(size)
1226    }
1227
1228    #[allow(clippy::too_many_lines)]
1229    fn update(
1230        &mut self,
1231        tree: &mut Tree,
1232        mut event: &Event,
1233        layout: Layout<'_>,
1234        cursor_position: mouse::Cursor,
1235        renderer: &Renderer,
1236        clipboard: &mut dyn Clipboard,
1237        shell: &mut Shell<'_, Message>,
1238        viewport: &iced::Rectangle,
1239    ) {
1240        let my_bounds = layout.bounds();
1241        let state = tree.state.downcast_mut::<LocalState>();
1242
1243        // The compositor dismissed our context menu popup: nothing else tells this state about it.
1244        #[cfg(wayland_platform)]
1245        if let iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland(
1246            iced::event::wayland::Event::Popup(iced::event::wayland::PopupEvent::Done, _, popup),
1247        )) = &event
1248        {
1249            let dismissed = state.menu_state.inner.with_data_mut(|data| {
1250                if data.popup_id.get(&self.window_id) == Some(popup) {
1251                    data.popup_id.clear();
1252                    data.reset();
1253                    true
1254                } else {
1255                    false
1256                }
1257            });
1258            if dismissed {
1259                state.show_context = None;
1260                for key in self.model.order.iter().copied() {
1261                    self.update_entity_paragraph(state, key);
1262                }
1263            }
1264        }
1265
1266        let hovered_before = state.hovered;
1267
1268        let my_id = self.get_drag_id();
1269
1270        if let Event::Dnd(e) = &mut event {
1271            let entity = state
1272                .dnd_state
1273                .drag_offer
1274                .as_ref()
1275                .map(|dnd_state| dnd_state.data);
1276            log::trace!(
1277                target: TAB_REORDER_LOG_TARGET,
1278                "segmented button {:?} received DnD event: {:?} entity={entity:?}",
1279                my_id,
1280                e
1281            );
1282            match e {
1283                DndEvent::Source(SourceEvent::Cancelled | SourceEvent::Finished) => {
1284                    if state.dragging_tab.take().is_some() {
1285                        state.tab_drag_candidate = None;
1286                        state.drop_hint = None;
1287                        self.emit_drop_hint(shell, state.drop_hint);
1288                        log::trace!(
1289                            target: TAB_REORDER_LOG_TARGET,
1290                            "tab drag source finished id={:?}",
1291                            my_id
1292                        );
1293                        shell.capture_event();
1294                        return;
1295                    }
1296                }
1297                DndEvent::Offer(
1298                    id,
1299                    OfferEvent::Enter {
1300                        x, y, mime_types, ..
1301                    },
1302                ) if Some(my_id) == *id => {
1303                    let entity = self
1304                        .variant_bounds(state, my_bounds)
1305                        .filter_map(|item| match item {
1306                            ItemBounds::Button(entity, bounds) => Some((entity, bounds)),
1307                            _ => None,
1308                        })
1309                        .find(|(_key, bounds)| bounds.contains(Point::new(*x as f32, *y as f32)))
1310                        .map(|(key, _)| key);
1311                    state.drop_hint = self.drop_hint_for_position(
1312                        state,
1313                        my_bounds,
1314                        Point::new(*x as f32, *y as f32),
1315                    );
1316                    self.emit_drop_hint(shell, state.drop_hint);
1317                    log::trace!(
1318                        target: TAB_REORDER_LOG_TARGET,
1319                        "offer enter id={my_id:?} entity={entity:?} @ ({x},{y}) mimes={mime_types:?}"
1320                    );
1321                    // force hovered state update
1322                    if let Some(entity) = entity {
1323                        state.hovered = Item::Tab(entity);
1324                        for key in self.model.order.iter().copied() {
1325                            self.update_entity_paragraph(state, key);
1326                        }
1327                    }
1328
1329                    let on_dnd_enter = self
1330                        .on_dnd_enter
1331                        .as_ref()
1332                        .zip(entity)
1333                        .map(|(on_enter, entity)| move |_, _, mimes| on_enter(entity, mimes));
1334                    let mimes = if let Some(mime) = self.tab_drag.as_ref().map(|d| &d.mime)
1335                        && mime_types.is_empty()
1336                    {
1337                        vec![mime.clone()]
1338                    } else {
1339                        mime_types.clone()
1340                    };
1341                    state.offer_mimes.clone_from(&mimes);
1342
1343                    _ = state
1344                        .dnd_state
1345                        .on_enter::<Message>(*x, *y, mimes, on_dnd_enter, entity);
1346                }
1347                DndEvent::Offer(id, OfferEvent::LeaveDestination) if Some(my_id) != *id => {}
1348                DndEvent::Offer(id, leave)
1349                    if matches!(leave, OfferEvent::Leave | OfferEvent::LeaveDestination)
1350                        && Some(my_id) == *id =>
1351                {
1352                    state.drop_hint = None;
1353                    self.emit_drop_hint(shell, state.drop_hint);
1354                    if let Some(Some(entity)) = entity {
1355                        if let Some(on_dnd_leave) = self.on_dnd_leave.as_ref() {
1356                            shell.publish(on_dnd_leave(entity));
1357                        }
1358                    }
1359                    log::trace!(
1360                        target: TAB_REORDER_LOG_TARGET,
1361                        "offer leave id={my_id:?} entity={entity:?}"
1362                    );
1363                    state.hovered = Item::None;
1364                    for key in self.model.order.iter().copied() {
1365                        self.update_entity_paragraph(state, key);
1366                    }
1367                    _ = state.dnd_state.on_leave::<Message>(None);
1368                }
1369                DndEvent::Offer(id, OfferEvent::Motion { x, y }) if Some(my_id) == *id => {
1370                    log::trace!(
1371                        target: TAB_REORDER_LOG_TARGET,
1372                        "offer motion id={my_id:?} cursor=({x},{y}) current_entity={entity:?}"
1373                    );
1374                    let new = self
1375                        .variant_bounds(state, my_bounds)
1376                        .filter_map(|item| match item {
1377                            ItemBounds::Button(entity, bounds) => Some((entity, bounds)),
1378                            _ => None,
1379                        })
1380                        .find(|(_key, bounds)| bounds.contains(Point::new(*x as f32, *y as f32)))
1381                        .map(|(key, _)| key);
1382                    if let Some(new_entity) = new {
1383                        state.dnd_state.on_motion::<Message>(
1384                            *x,
1385                            *y,
1386                            None::<fn(_, _) -> Message>,
1387                            None::<fn(_, _, _) -> Message>,
1388                            Some(new_entity),
1389                        );
1390                        state.drop_hint = self.drop_hint_for_position(
1391                            state,
1392                            my_bounds,
1393                            Point::new(*x as f32, *y as f32),
1394                        );
1395                        self.emit_drop_hint(shell, state.drop_hint);
1396                        if Some(Some(new_entity)) != entity {
1397                            state.hovered = Item::Tab(new_entity);
1398                            for key in self.model.order.iter().copied() {
1399                                self.update_entity_paragraph(state, key);
1400                            }
1401                            let prev_action = state
1402                                .dnd_state
1403                                .drag_offer
1404                                .as_ref()
1405                                .map(|dnd| dnd.selected_action);
1406                            if let Some(on_dnd_enter) = self.on_dnd_enter.as_ref() {
1407                                shell.publish(on_dnd_enter(new_entity, state.offer_mimes.clone()));
1408                            }
1409                            if let Some(dnd) = state.dnd_state.drag_offer.as_mut() {
1410                                dnd.data = Some(new_entity);
1411                                if let Some(prev_action) = prev_action {
1412                                    dnd.selected_action = prev_action;
1413                                }
1414                            }
1415                        }
1416                    } else if entity.is_some() {
1417                        state.hovered = Item::None;
1418                        for key in self.model.order.iter().copied() {
1419                            self.update_entity_paragraph(state, key);
1420                        }
1421                        log::trace!(
1422                            target: TAB_REORDER_LOG_TARGET,
1423                            "offer motion leaving id={my_id:?}"
1424                        );
1425                        state.drop_hint = None;
1426                        self.emit_drop_hint(shell, state.drop_hint);
1427                        state.dnd_state.on_motion::<Message>(
1428                            *x,
1429                            *y,
1430                            None::<fn(_, _) -> Message>,
1431                            None::<fn(_, _, _) -> Message>,
1432                            None,
1433                        );
1434                        if let Some(on_dnd_leave) = self.on_dnd_leave.as_ref() {
1435                            if let Some(Some(entity)) = entity {
1436                                shell.publish(on_dnd_leave(entity));
1437                            }
1438                        }
1439                    }
1440                }
1441                DndEvent::Offer(id, OfferEvent::Drop) if Some(my_id) == *id => {
1442                    log::trace!(
1443                        target: TAB_REORDER_LOG_TARGET,
1444                        "offer drop id={my_id:?} entity={entity:?}"
1445                    );
1446                    _ = state
1447                        .dnd_state
1448                        .on_drop::<Message>(None::<fn(_, _) -> Message>);
1449                }
1450                DndEvent::Offer(id, OfferEvent::SelectedAction(action)) if Some(my_id) == *id => {
1451                    if state.dnd_state.drag_offer.is_some() {
1452                        log::trace!(
1453                            target: TAB_REORDER_LOG_TARGET,
1454                            "offer selected action id={my_id:?} action={action:?} entity={entity:?}"
1455                        );
1456                        _ = state
1457                            .dnd_state
1458                            .on_action_selected::<Message>(*action, None::<fn(_) -> Message>);
1459                    }
1460                }
1461                DndEvent::Offer(id, OfferEvent::Data { data, mime_type }) if Some(my_id) == *id => {
1462                    log::trace!(
1463                        target: TAB_REORDER_LOG_TARGET,
1464                        "offer data id={my_id:?} entity={entity:?} mime={mime_type:?}"
1465                    );
1466                    let drop_entity = entity
1467                        .flatten()
1468                        .or_else(|| state.drop_hint.map(|hint| hint.entity));
1469                    let allow_reorder = state
1470                        .dnd_state
1471                        .drag_offer
1472                        .as_ref()
1473                        .is_some_and(|offer| offer.selected_action.contains(DndAction::Move));
1474                    let pending_reorder = if allow_reorder
1475                        && self.on_reorder.is_some()
1476                        && self.tab_drag.as_ref().is_some_and(|d| d.mime == *mime_type)
1477                        && state.dragging_tab.is_some()
1478                    {
1479                        drop_entity.and_then(|target| self.reorder_event_for_drop(state, target))
1480                    } else {
1481                        None
1482                    };
1483                    if let Some(entity) = drop_entity {
1484                        let on_drop = self.on_dnd_drop.as_ref();
1485                        let on_drop = on_drop.map(|on_drop| {
1486                            |mime, data, action, _, _| on_drop(entity, data, mime, action)
1487                        });
1488
1489                        let (maybe_msg, ret) = state.dnd_state.on_data_received(
1490                            mime_type.clone(),
1491                            data.clone(),
1492                            None::<fn(_, _) -> Message>,
1493                            on_drop,
1494                        );
1495                        if matches!(ret, iced::event::Status::Captured) {
1496                            shell.capture_event();
1497                        }
1498                        if let Some(msg) = maybe_msg {
1499                            log::trace!(
1500                                target: TAB_REORDER_LOG_TARGET,
1501                                "publishing drop message entity={entity:?}"
1502                            );
1503                            shell.publish(msg);
1504                        }
1505                        state.drop_hint = None;
1506
1507                        self.emit_drop_hint(shell, state.drop_hint);
1508                        if let Some(event) = pending_reorder {
1509                            state.focused_item = Item::Tab(event.dragged);
1510                            state.hovered = Item::None;
1511                            for key in self.model.order.iter().copied() {
1512                                self.update_entity_paragraph(state, key);
1513                            }
1514                            if let Some(on_reorder) = self.on_reorder.as_ref() {
1515                                shell.publish(on_reorder(event));
1516                                shell.capture_event();
1517                                return;
1518                            }
1519                        }
1520                        return;
1521                    }
1522                }
1523                _ => {}
1524            }
1525        }
1526
1527        if cursor_position.is_over(my_bounds) {
1528            let fingers_pressed = state.fingers_pressed.len();
1529
1530            match event {
1531                Event::Touch(touch::Event::FingerPressed { id, .. }) => {
1532                    state.fingers_pressed.insert(*id);
1533                }
1534
1535                Event::Touch(touch::Event::FingerLifted { id, .. }) => {
1536                    state.fingers_pressed.remove(id);
1537                }
1538                _ => (),
1539            }
1540
1541            // Check for clicks on the previous and next tab buttons, when tabs are collapsed.
1542            if state.collapsed {
1543                // Check if the prev tab button was clicked.
1544                if cursor_position
1545                    .is_over(prev_tab_bounds(&my_bounds, f32::from(self.button_height)))
1546                    && self.prev_tab_sensitive(state)
1547                {
1548                    state.hovered = Item::PrevButton;
1549                    for key in self.model.order.iter().copied() {
1550                        self.update_entity_paragraph(state, key);
1551                    }
1552                    if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
1553                    | Event::Touch(touch::Event::FingerLifted { .. }) = event
1554                    {
1555                        state.buttons_offset -= 1;
1556                    }
1557                } else {
1558                    // Check if the next tab button was clicked.
1559                    if cursor_position
1560                        .is_over(next_tab_bounds(&my_bounds, f32::from(self.button_height)))
1561                        && self.next_tab_sensitive(state)
1562                    {
1563                        state.hovered = Item::NextButton;
1564                        for key in self.model.order.iter().copied() {
1565                            self.update_entity_paragraph(state, key);
1566                        }
1567                        if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
1568                        | Event::Touch(touch::Event::FingerLifted { .. }) = event
1569                        {
1570                            state.buttons_offset += 1;
1571                        }
1572                    }
1573                }
1574            }
1575
1576            for (key, bounds) in self
1577                .variant_bounds(state, my_bounds)
1578                .filter_map(|item| match item {
1579                    ItemBounds::Button(entity, bounds) => Some((entity, bounds)),
1580                    _ => None,
1581                })
1582                .collect::<Vec<_>>()
1583            {
1584                if cursor_position.is_over(bounds) {
1585                    if self.model.items[key].enabled {
1586                        // Record that the mouse is hovering over this button.
1587                        if state.hovered != Item::Tab(key) {
1588                            state.hovered = Item::Tab(key);
1589                            for key in self.model.order.iter().copied() {
1590                                self.update_entity_paragraph(state, key);
1591                            }
1592                        }
1593
1594                        let close_button_bounds =
1595                            close_bounds(bounds, f32::from(self.close_icon.size));
1596                        let over_close_button = self.model.items[key].closable
1597                            && cursor_position.is_over(close_button_bounds);
1598
1599                        // If marked as closable, show a close icon.
1600                        if self.model.items[key].closable {
1601                            // Emit close message if the close button is pressed.
1602                            if let Some(on_close) = self.on_close.as_ref() {
1603                                if over_close_button
1604                                    && (left_button_released(&event)
1605                                        || (touch_lifted(&event) && fingers_pressed == 1))
1606                                {
1607                                    shell.publish(on_close(key));
1608                                    shell.capture_event();
1609                                    return;
1610                                }
1611
1612                                if self.on_middle_press.is_none() {
1613                                    // Emit close message if the tab is middle clicked.
1614                                    if let Event::Mouse(mouse::Event::ButtonReleased(
1615                                        mouse::Button::Middle,
1616                                    )) = event
1617                                    {
1618                                        if state.middle_clicked == Some(Item::Tab(key)) {
1619                                            shell.publish(on_close(key));
1620                                            shell.capture_event();
1621                                            return;
1622                                        }
1623
1624                                        state.middle_clicked = None;
1625                                    }
1626                                }
1627                            }
1628                        }
1629
1630                        if self.tab_drag.is_some()
1631                            && matches!(
1632                                event,
1633                                Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
1634                            )
1635                            && !over_close_button
1636                            && let Some(position) = cursor_position.position()
1637                        {
1638                            state.tab_drag_candidate = Some(TabDragCandidate {
1639                                entity: key,
1640                                bounds,
1641                                origin: position,
1642                            });
1643                            if let Some(tab_drag) = self.tab_drag.as_ref() {
1644                                log::trace!(
1645                                    target: TAB_REORDER_LOG_TARGET,
1646                                    "tab drag candidate entity={:?} origin=({:.2},{:.2}) bounds=({:.2},{:.2},{:.2},{:.2}) threshold={}",
1647                                    key,
1648                                    position.x,
1649                                    position.y,
1650                                    bounds.x,
1651                                    bounds.y,
1652                                    bounds.width,
1653                                    bounds.height,
1654                                    tab_drag.threshold
1655                                );
1656                            }
1657                        }
1658
1659                        if is_lifted(&event) {
1660                            state.unfocus();
1661                        }
1662
1663                        #[cfg(wayland_platform)]
1664                        if is_pressed(event)
1665                            && let Some(on_context) = self.on_context.as_ref()
1666                        {
1667                            let (was_open, id) = state.menu_state.inner.with_data_mut(|data| {
1668                                let was_open = data.open;
1669                                data.reset();
1670                                data.open = false;
1671                                data.view_cursor = cursor_position;
1672                                let root = data.popup_id.remove(&self.window_id);
1673                                data.popup_id.clear();
1674                                (was_open, root)
1675                            });
1676                            if let Some(w) = id
1677                                && let Some(surface_action) = self.on_surface_action.as_ref()
1678                                && was_open
1679                            {
1680                                use crate::surface::action::destroy_popup;
1681
1682                                shell.publish((surface_action)(destroy_popup(w)));
1683                                return;
1684                            }
1685                        }
1686
1687                        if let Some(on_activate) = self.on_activate.as_ref() {
1688                            if is_pressed(event) {
1689                                state.pressed_item = Some(Item::Tab(key));
1690                            } else if is_lifted(&event) && self.button_is_pressed(state, key) {
1691                                shell.publish(on_activate(key));
1692                                state.set_focused();
1693                                state.focused_item = Item::Tab(key);
1694                                state.pressed_item = None;
1695                                shell.capture_event();
1696                                return;
1697                            }
1698                        }
1699
1700                        // Present a context menu on a right click event.
1701                        if self.context_menu.is_some()
1702                            && let Some(on_context) = self.on_context.as_ref()
1703                            && (right_button_released(&event)
1704                                || (touch_lifted(&event) && fingers_pressed == 2))
1705                        {
1706                            state.show_context = Some(key);
1707                            state.context_cursor = cursor_position.position().unwrap_or_default();
1708
1709                            state.menu_state.inner.with_data_mut(|data| {
1710                                // Clear stale MenuBounds from any previous context menu before opening a new one.
1711                                data.reset();
1712                                data.open = true;
1713                                data.view_cursor = cursor_position;
1714                            });
1715
1716                            shell.publish(on_context(key));
1717                            shell.capture_event();
1718
1719                            #[cfg(wayland_platform)]
1720                            if matches!(
1721                                crate::app::cosmic::WINDOWING_SYSTEM.get(),
1722                                Some(crate::app::cosmic::WindowingSystem::Wayland)
1723                            ) {
1724                                self.create_popup(
1725                                    layout,
1726                                    cursor_position,
1727                                    renderer,
1728                                    shell,
1729                                    viewport,
1730                                    tree,
1731                                );
1732                            }
1733                            return;
1734                        }
1735                        if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) =
1736                            event
1737                        {
1738                            state.middle_clicked = Some(Item::Tab(key));
1739                            if let Some(on_middle_press) = self.on_middle_press.as_ref() {
1740                                shell.publish(on_middle_press(key));
1741                                shell.capture_event();
1742                                return;
1743                            }
1744                        }
1745                    }
1746
1747                    break;
1748                } else if state.hovered == Item::Tab(key) {
1749                    state.hovered = Item::None;
1750                    self.update_entity_paragraph(state, key);
1751                }
1752            }
1753
1754            if self.scrollable_focus
1755                && let Some(on_activate) = self.on_activate.as_ref()
1756                && let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event
1757            {
1758                let current = Instant::now();
1759
1760                // Permit successive scroll wheel events only after a given delay.
1761                if state.wheel_timestamp.is_none_or(|previous| {
1762                    current.duration_since(previous) > Duration::from_millis(250)
1763                }) {
1764                    state.wheel_timestamp = Some(current);
1765
1766                    match delta {
1767                        ScrollDelta::Lines { y, .. } | ScrollDelta::Pixels { y, .. } => {
1768                            let mut activate_key = None;
1769
1770                            if *y < 0.0 {
1771                                let mut prev_key = Entity::null();
1772
1773                                for key in self.model.order.iter().copied() {
1774                                    if self.model.is_active(key) && !prev_key.is_null() {
1775                                        activate_key = Some(prev_key);
1776                                    }
1777
1778                                    if self.model.is_enabled(key) {
1779                                        prev_key = key;
1780                                    }
1781                                }
1782                            } else if *y > 0.0 {
1783                                let mut buttons = self.model.order.iter().copied();
1784                                while let Some(key) = buttons.next() {
1785                                    if self.model.is_active(key) {
1786                                        for key in buttons {
1787                                            if self.model.is_enabled(key) {
1788                                                activate_key = Some(key);
1789                                                break;
1790                                            }
1791                                        }
1792                                        break;
1793                                    }
1794                                }
1795                            }
1796
1797                            if let Some(key) = activate_key {
1798                                shell.publish(on_activate(key));
1799                                state.set_focused();
1800                                state.focused_item = Item::Tab(key);
1801                                shell.capture_event();
1802                                return;
1803                            }
1804                        }
1805                    }
1806                }
1807            }
1808        } else {
1809            if let Item::Tab(_key) = std::mem::replace(&mut state.hovered, Item::None) {
1810                for key in self.model.order.iter().copied() {
1811                    self.update_entity_paragraph(state, key);
1812                }
1813            }
1814            if state.is_focused() {
1815                // Unfocus on clicks outside of the boundaries of the segmented button.
1816                if is_pressed(&event) {
1817                    state.unfocus();
1818                    state.pressed_item = None;
1819                    return;
1820                }
1821            } else if is_lifted(&event) {
1822                state.pressed_item = None;
1823            }
1824        }
1825
1826        if let (Some(tab_drag), Some(candidate)) =
1827            (self.tab_drag.as_ref(), state.tab_drag_candidate)
1828            && let Event::Mouse(mouse::Event::CursorMoved { .. }) = event
1829            && let Some(position) = cursor_position.position()
1830            && position.distance(candidate.origin) >= tab_drag.threshold
1831            && let Some(candidate) = state.tab_drag_candidate.take()
1832        {
1833            log::trace!(
1834                target: TAB_REORDER_LOG_TARGET,
1835                "tab drag threshold met entity={:?} distance={:.2} threshold={}",
1836                candidate.entity,
1837                position.distance(candidate.origin),
1838                tab_drag.threshold
1839            );
1840            if self.start_tab_drag(
1841                state,
1842                candidate.entity,
1843                candidate.bounds,
1844                position,
1845                clipboard,
1846            ) {
1847                shell.capture_event();
1848                return;
1849            }
1850        }
1851
1852        if (matches!(event, Event::Mouse(mouse::Event::ButtonReleased(_))) || (touch_lifted(event)))
1853            && let Some(_id) = state
1854                .menu_state
1855                .inner
1856                .with_data_mut(|ms| ms.popup_id.remove(&self.window_id))
1857        {
1858            #[cfg(wayland_platform)]
1859            {
1860                let surface_action = self.on_surface_action.as_ref().unwrap();
1861                shell.capture_event();
1862
1863                shell.publish(surface_action(crate::surface::action::destroy_popup(_id)));
1864            }
1865            state.show_context = None;
1866
1867            state.menu_state.inner.with_data_mut(|data| {
1868                // Clear stale MenuBounds from any previous context menu before opening a new one.
1869                data.reset();
1870                data.open = false;
1871                data.view_cursor = cursor_position;
1872            });
1873        }
1874
1875        if matches!(
1876            event,
1877            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
1878        ) {
1879            state.tab_drag_candidate = None;
1880        }
1881
1882        if state.is_focused() {
1883            if let Event::Keyboard(keyboard::Event::KeyPressed {
1884                key: keyboard::Key::Named(keyboard::key::Named::Tab),
1885                modifiers,
1886                ..
1887            }) = event
1888            {
1889                shell.request_redraw();
1890                state.focused_visible = true;
1891                return if *modifiers == keyboard::Modifiers::SHIFT {
1892                    self.focus_previous(state, shell);
1893                } else if modifiers.is_empty() {
1894                    self.focus_next(state, shell);
1895                };
1896            }
1897
1898            if let Some(on_activate) = self.on_activate.as_ref()
1899                && let Event::Keyboard(keyboard::Event::KeyReleased {
1900                    key: keyboard::Key::Named(keyboard::key::Named::Enter),
1901                    ..
1902                }) = event
1903            {
1904                match state.focused_item {
1905                    Item::Tab(entity) => {
1906                        shell.publish(on_activate(entity));
1907                    }
1908
1909                    Item::PrevButton => {
1910                        if self.prev_tab_sensitive(state) {
1911                            state.buttons_offset -= 1;
1912
1913                            // If the change would cause it to be insensitive, focus the first tab.
1914                            if !self.prev_tab_sensitive(state)
1915                                && let Some(first) = self.first_tab(state)
1916                            {
1917                                state.focused_item = Item::Tab(first);
1918                            }
1919                        }
1920                    }
1921
1922                    Item::NextButton => {
1923                        if self.next_tab_sensitive(state) {
1924                            state.buttons_offset += 1;
1925
1926                            // If the change would cause it to be insensitive, focus the last tab.
1927                            if !self.next_tab_sensitive(state)
1928                                && let Some(last) = self.last_tab(state)
1929                            {
1930                                state.focused_item = Item::Tab(last);
1931                            }
1932                        }
1933                    }
1934
1935                    Item::None | Item::Set => (),
1936                }
1937
1938                shell.capture_event();
1939            }
1940        }
1941
1942        if hovered_before != state.hovered {
1943            shell.request_redraw();
1944        }
1945    }
1946
1947    fn operate(
1948        &mut self,
1949        tree: &mut Tree,
1950        layout: Layout<'_>,
1951        _renderer: &Renderer,
1952        operation: &mut dyn iced_core::widget::Operation<()>,
1953    ) {
1954        let state = tree.state.downcast_mut::<LocalState>();
1955        operation.focusable(Some(&self.id.0), layout.bounds(), state);
1956        operation.custom(Some(&self.id.0), layout.bounds(), state);
1957
1958        if let Item::Set = state.focused_item {
1959            if self.prev_tab_sensitive(state) {
1960                state.focused_item = Item::PrevButton;
1961            } else if let Some(first) = self.first_tab(state) {
1962                state.focused_item = Item::Tab(first);
1963            }
1964        }
1965    }
1966
1967    fn mouse_interaction(
1968        &self,
1969        tree: &Tree,
1970        layout: Layout<'_>,
1971        cursor_position: mouse::Cursor,
1972        _viewport: &iced::Rectangle,
1973        _renderer: &Renderer,
1974    ) -> iced_core::mouse::Interaction {
1975        if self.on_activate.is_none() {
1976            return iced_core::mouse::Interaction::default();
1977        }
1978        let state = tree.state.downcast_ref::<LocalState>();
1979        let bounds = layout.bounds();
1980
1981        if cursor_position.is_over(bounds) {
1982            let hovered_button = self
1983                .variant_bounds(state, bounds)
1984                .filter_map(|item| match item {
1985                    ItemBounds::Button(entity, bounds) => Some((entity, bounds)),
1986                    _ => None,
1987                })
1988                .find(|(_key, bounds)| cursor_position.is_over(*bounds));
1989
1990            if let Some((key, _bounds)) = hovered_button {
1991                return if self.model.items[key].enabled {
1992                    iced_core::mouse::Interaction::Pointer
1993                } else {
1994                    iced_core::mouse::Interaction::Idle
1995                };
1996            }
1997        }
1998
1999        iced_core::mouse::Interaction::default()
2000    }
2001
2002    #[allow(clippy::too_many_lines)]
2003    fn draw(
2004        &self,
2005        tree: &Tree,
2006        renderer: &mut Renderer,
2007        theme: &crate::Theme,
2008        style: &renderer::Style,
2009        layout: Layout<'_>,
2010        cursor: mouse::Cursor,
2011        viewport: &iced::Rectangle,
2012    ) {
2013        let state = tree.state.downcast_ref::<LocalState>();
2014        let appearance = Self::variant_appearance(theme, &self.style);
2015        let bounds: Rectangle = layout.bounds();
2016        let button_amount = self.model.items.len();
2017        let show_drop_hint = state.dragging_tab.is_some();
2018        let drop_hint = if show_drop_hint {
2019            state.drop_hint
2020        } else {
2021            None
2022        };
2023
2024        // Draw the background, if a background was defined.
2025        if let Some(background) = appearance.background {
2026            renderer.fill_quad(
2027                renderer::Quad {
2028                    bounds,
2029                    border: appearance.border,
2030                    shadow: Shadow::default(),
2031                    snap: true,
2032                },
2033                background,
2034            );
2035        }
2036
2037        // Draw previous and next tab buttons if there is a need to paginate tabs.
2038        if state.collapsed {
2039            let mut tab_bounds = prev_tab_bounds(&bounds, f32::from(self.button_height));
2040
2041            // Previous tab button
2042            let mut background_appearance =
2043                if self.on_activate.is_some() && Item::PrevButton == state.focused_item {
2044                    Some(appearance.active)
2045                } else if self.on_activate.is_some() && Item::PrevButton == state.hovered {
2046                    Some(appearance.hover)
2047                } else {
2048                    None
2049                };
2050
2051            if let Some(background_appearance) = background_appearance.take() {
2052                renderer.fill_quad(
2053                    renderer::Quad {
2054                        bounds: tab_bounds,
2055                        border: Border {
2056                            radius: theme.cosmic().radius_s().into(),
2057                            ..Default::default()
2058                        },
2059                        shadow: Shadow::default(),
2060                        snap: true,
2061                    },
2062                    background_appearance
2063                        .background
2064                        .unwrap_or(Background::Color(Color::TRANSPARENT)),
2065                );
2066            }
2067
2068            draw_icon::<Message>(
2069                renderer,
2070                theme,
2071                style,
2072                cursor,
2073                viewport,
2074                if state.buttons_offset == 0 {
2075                    appearance.inactive.text_color
2076                } else {
2077                    appearance.active.text_color
2078                },
2079                Rectangle {
2080                    x: tab_bounds.x + 8.0,
2081                    y: tab_bounds.y + f32::from(self.button_height) / 4.0,
2082                    width: 16.0,
2083                    height: 16.0,
2084                },
2085                icon::from_name("go-previous-symbolic").size(16).icon(),
2086            );
2087
2088            tab_bounds = next_tab_bounds(&bounds, f32::from(self.button_height));
2089
2090            // Next tab button
2091            background_appearance =
2092                if self.on_activate.is_some() && Item::NextButton == state.focused_item {
2093                    Some(appearance.active)
2094                } else if self.on_activate.is_some() && Item::NextButton == state.hovered {
2095                    Some(appearance.hover)
2096                } else {
2097                    None
2098                };
2099
2100            if let Some(background_appearance) = background_appearance {
2101                renderer.fill_quad(
2102                    renderer::Quad {
2103                        bounds: tab_bounds,
2104                        border: Border {
2105                            radius: theme.cosmic().radius_s().into(),
2106                            ..Default::default()
2107                        },
2108                        shadow: Shadow::default(),
2109                        snap: true,
2110                    },
2111                    background_appearance
2112                        .background
2113                        .unwrap_or(Background::Color(Color::TRANSPARENT)),
2114                );
2115            }
2116
2117            draw_icon::<Message>(
2118                renderer,
2119                theme,
2120                style,
2121                cursor,
2122                viewport,
2123                if self.next_tab_sensitive(state) {
2124                    appearance.active.text_color
2125                } else if let Item::NextButton = state.focused_item {
2126                    appearance.active.text_color
2127                } else {
2128                    appearance.inactive.text_color
2129                },
2130                Rectangle {
2131                    x: tab_bounds.x + 8.0,
2132                    y: tab_bounds.y + f32::from(self.button_height) / 4.0,
2133                    width: 16.0,
2134                    height: 16.0,
2135                },
2136                icon::from_name("go-next-symbolic").size(16).icon(),
2137            );
2138        }
2139
2140        let rad_0 = THEME.lock().unwrap().cosmic().corner_radii.radius_0;
2141
2142        let divider_background = Background::Color(
2143            crate::theme::active()
2144                .cosmic()
2145                .primary_component_divider()
2146                .into(),
2147        );
2148
2149        // Draw each of the items in the widget.
2150        let mut nth = 0;
2151        let drop_hint_marker = drop_hint;
2152        let show_drop_hint_marker = show_drop_hint;
2153        self.variant_bounds(state, bounds).for_each(move |item| {
2154            let (key, mut bounds) = match item {
2155                // Draw a button
2156                ItemBounds::Button(entity, bounds) => (entity, bounds),
2157
2158                // Draw a divider between buttons
2159                ItemBounds::Divider(bounds, accented) => {
2160                    renderer.fill_quad(
2161                        renderer::Quad {
2162                            bounds,
2163                            border: Border::default(),
2164                            shadow: Shadow::default(),
2165                            snap: true,
2166                        },
2167                        {
2168                            let theme = crate::theme::active();
2169                            if accented {
2170                                Background::Color(theme.cosmic().small_widget_divider().into())
2171                            } else {
2172                                Background::Color(theme.cosmic().primary_container_divider().into())
2173                            }
2174                        },
2175                    );
2176
2177                    return;
2178                }
2179            };
2180
2181            let original_bounds = bounds;
2182            let center_y = bounds.center_y();
2183
2184            if show_drop_hint_marker
2185                && matches!(
2186                    drop_hint_marker,
2187                    Some(DropHint {
2188                        entity,
2189                        side: DropSide::Before
2190                    }) if entity == key
2191                )
2192            {
2193                draw_drop_indicator(
2194                    renderer,
2195                    original_bounds,
2196                    DropSide::Before,
2197                    Self::VERTICAL,
2198                    appearance.active.text_color,
2199                );
2200            }
2201
2202            let menu_open = || {
2203                state.show_context == Some(key)
2204                    && state.menu_state.inner.with_data(|data| data.open)
2205            };
2206
2207            let key_is_active = self.model.is_active(key);
2208            let key_is_focused = state.focused_visible && self.button_is_focused(state, key);
2209            let key_is_hovered = self.button_is_hovered(state, key);
2210            let status_appearance = if self.button_is_pressed(state, key) {
2211                appearance.pressed
2212            } else if key_is_hovered || menu_open() {
2213                appearance.hover
2214            } else if key_is_active {
2215                appearance.active
2216            } else {
2217                appearance.inactive
2218            };
2219
2220            let button_appearance = if nth == 0 {
2221                status_appearance.first
2222            } else if nth + 1 == button_amount {
2223                status_appearance.last
2224            } else {
2225                status_appearance.middle
2226            };
2227
2228            // Draw the active hint on tabs
2229            if appearance.active_width > 0.0 {
2230                let active_width = if key_is_active {
2231                    appearance.active_width
2232                } else {
2233                    1.0
2234                };
2235
2236                renderer.fill_quad(
2237                    renderer::Quad {
2238                        bounds: if Self::VERTICAL {
2239                            Rectangle {
2240                                x: bounds.x + bounds.width - active_width,
2241                                width: active_width,
2242                                ..bounds
2243                            }
2244                        } else {
2245                            Rectangle {
2246                                y: bounds.y + bounds.height - active_width,
2247                                height: active_width,
2248                                ..bounds
2249                            }
2250                        },
2251                        border: Border {
2252                            radius: rad_0.into(),
2253                            ..Default::default()
2254                        },
2255                        shadow: Shadow::default(),
2256                        snap: true,
2257                    },
2258                    appearance.active.text_color,
2259                );
2260            }
2261
2262            bounds.x += f32::from(self.button_padding[0]);
2263            bounds.width -= f32::from(self.button_padding[0]) - f32::from(self.button_padding[2]);
2264            let mut indent_padding = 0.0;
2265
2266            // Adjust bounds by indent
2267            if let Some(indent) = self.model.indent(key)
2268                && indent > 0
2269            {
2270                let adjustment = f32::from(indent) * f32::from(self.indent_spacing);
2271                bounds.x += adjustment;
2272                bounds.width -= adjustment;
2273
2274                // Draw indent line
2275                if let crate::theme::SegmentedButton::FileNav = self.style
2276                    && indent > 1
2277                {
2278                    indent_padding = 7.0;
2279
2280                    for level in 1..indent {
2281                        renderer.fill_quad(
2282                            renderer::Quad {
2283                                bounds: Rectangle {
2284                                    x: (level as f32)
2285                                        .mul_add(-(self.indent_spacing as f32), bounds.x)
2286                                        + indent_padding,
2287                                    width: 1.0,
2288                                    ..bounds
2289                                },
2290                                border: Border {
2291                                    radius: rad_0.into(),
2292                                    ..Default::default()
2293                                },
2294                                shadow: Shadow::default(),
2295                                snap: true,
2296                            },
2297                            divider_background,
2298                        );
2299                    }
2300
2301                    indent_padding += 4.0;
2302                }
2303            }
2304
2305            // Render the background of the button.
2306            if key_is_focused || status_appearance.background.is_some() {
2307                renderer.fill_quad(
2308                    renderer::Quad {
2309                        bounds: Rectangle {
2310                            x: bounds.x - f32::from(self.button_padding[0]) + indent_padding,
2311                            width: bounds.width + f32::from(self.button_padding[0])
2312                                - f32::from(self.button_padding[2])
2313                                - indent_padding,
2314                            ..bounds
2315                        },
2316                        border: if key_is_focused {
2317                            Border {
2318                                width: 1.0,
2319                                color: appearance.active.text_color,
2320                                radius: button_appearance.border.radius,
2321                            }
2322                        } else {
2323                            button_appearance.border
2324                        },
2325                        shadow: Shadow::default(),
2326                        snap: true,
2327                    },
2328                    status_appearance
2329                        .background
2330                        .unwrap_or(Background::Color(Color::TRANSPARENT)),
2331                );
2332            }
2333
2334            // Align contents of the button to the requested `button_alignment`.
2335            {
2336                // Avoid shifting content outside the left edge when the measured content is
2337                // wider than the available button bounds (for example, non-ellipsized text).
2338                let actual_width = state.internal_layout[nth].1.width.min(bounds.width);
2339
2340                let offset = match self.button_alignment {
2341                    Alignment::Start => None,
2342                    Alignment::Center => Some((bounds.width - actual_width) / 2.0),
2343                    Alignment::End => Some(bounds.width - actual_width),
2344                };
2345
2346                if let Some(offset) = offset {
2347                    bounds.x += offset - f32::from(self.button_padding[0]);
2348                    bounds.width = actual_width;
2349                }
2350            }
2351
2352            // Draw the image beside the text.
2353            if let Some(icon) = self.model.icon(key) {
2354                let mut image_bounds = bounds;
2355                let width = f32::from(icon.size);
2356                let offset = width + f32::from(self.button_spacing);
2357                image_bounds.y = center_y - width / 2.0;
2358
2359                draw_icon::<Message>(
2360                    renderer,
2361                    theme,
2362                    style,
2363                    cursor,
2364                    viewport,
2365                    status_appearance.text_color,
2366                    Rectangle {
2367                        width,
2368                        height: width,
2369                        ..image_bounds
2370                    },
2371                    icon.clone(),
2372                );
2373
2374                bounds.x += offset;
2375            } else {
2376                // Draw the selection indicator if widget is a segmented selection, and the item is selected.
2377                if key_is_active && let crate::theme::SegmentedButton::Control = self.style {
2378                    let mut image_bounds = bounds;
2379                    image_bounds.y = center_y - 8.0;
2380
2381                    draw_icon::<Message>(
2382                        renderer,
2383                        theme,
2384                        style,
2385                        cursor,
2386                        viewport,
2387                        status_appearance.text_color,
2388                        Rectangle {
2389                            width: 16.0,
2390                            height: 16.0,
2391                            ..image_bounds
2392                        },
2393                        crate::widget::icon(match crate::widget::common::object_select().data() {
2394                            iced_core::svg::Data::Bytes(bytes) => {
2395                                crate::widget::icon::from_svg_bytes(bytes.as_ref()).symbolic(true)
2396                            }
2397                            iced_core::svg::Data::Path(path) => {
2398                                crate::widget::icon::from_path(path.clone())
2399                            }
2400                        }),
2401                    );
2402
2403                    let offset = 16.0 + f32::from(self.button_spacing);
2404
2405                    bounds.x += offset;
2406                }
2407            }
2408
2409            // Whether to show the close button on this tab.
2410            let show_close_button =
2411                (key_is_active || !self.show_close_icon_on_hover || key_is_hovered)
2412                    && self.model.is_closable(key);
2413
2414            // Width of the icon used by the close button, which we will subtract from the text bounds.
2415            let close_icon_width = if show_close_button {
2416                f32::from(self.close_icon.size)
2417            } else {
2418                0.0
2419            };
2420
2421            bounds.width = original_bounds.width
2422                - (bounds.x - original_bounds.x)
2423                - close_icon_width
2424                - f32::from(self.button_padding[2]);
2425
2426            bounds.y = center_y;
2427
2428            if self.model.text(key).is_some_and(|text| !text.is_empty()) {
2429                // FIXME why has this behavior changed? Does the center alignment not work with infinite bounds now?
2430                bounds.y -= state.paragraphs[key].min_height() / 2.;
2431
2432                // Draw the text for this segmented button or tab.
2433                renderer.fill_paragraph(
2434                    state.paragraphs[key].raw(),
2435                    bounds.position(),
2436                    status_appearance.text_color,
2437                    Rectangle {
2438                        x: bounds.x,
2439                        width: bounds.width,
2440                        height: original_bounds.height,
2441                        y: bounds.y,
2442                        //  ..original_bounds,
2443                    },
2444                );
2445            }
2446
2447            // Draw a close button if set.
2448            if show_close_button {
2449                let close_button_bounds = close_bounds(original_bounds, close_icon_width);
2450
2451                draw_icon::<Message>(
2452                    renderer,
2453                    theme,
2454                    style,
2455                    cursor,
2456                    viewport,
2457                    status_appearance.text_color,
2458                    close_button_bounds,
2459                    self.close_icon.clone(),
2460                );
2461            }
2462
2463            if show_drop_hint_marker {
2464                if matches!(
2465                    drop_hint_marker,
2466                    Some(DropHint {
2467                        entity,
2468                        side: DropSide::After
2469                    }) if entity == key
2470                ) {
2471                    draw_drop_indicator(
2472                        renderer,
2473                        original_bounds,
2474                        DropSide::After,
2475                        Self::VERTICAL,
2476                        appearance.active.text_color,
2477                    );
2478                }
2479            }
2480
2481            nth += 1;
2482        });
2483    }
2484
2485    fn overlay<'b>(
2486        &'b mut self,
2487        tree: &'b mut Tree,
2488        layout: iced_core::Layout<'b>,
2489        _renderer: &Renderer,
2490        _viewport: &iced_core::Rectangle,
2491        translation: Vector,
2492    ) -> Option<iced_core::overlay::Element<'b, Message, crate::Theme, Renderer>> {
2493        #[cfg(wayland_platform)]
2494        if matches!(
2495            crate::app::cosmic::WINDOWING_SYSTEM.get(),
2496            Some(crate::app::cosmic::WindowingSystem::Wayland)
2497        ) && self.on_surface_action.is_some()
2498            && self.window_id != window::Id::NONE
2499        {
2500            return None;
2501        }
2502
2503        let state = tree.state.downcast_mut::<LocalState>();
2504        let menu_state = state.menu_state.clone();
2505
2506        let entity = state.show_context?;
2507
2508        let (mut bounds, i) = self
2509            .variant_bounds(state, layout.bounds())
2510            .filter_map(|item| match item {
2511                ItemBounds::Button(entity, bounds) => Some((bounds, entity)),
2512                _ => None,
2513            })
2514            .enumerate()
2515            .find_map(|(i, (bounds, e))| if e == entity { Some((bounds, i)) } else { None })?;
2516
2517        assert!(
2518            self.context_menu
2519                .as_ref()
2520                .is_none_or(|m| m[0].children.len() == self.model.len())
2521        );
2522        let menu = self
2523            .context_menu
2524            .as_mut()
2525            .map(|m| m[0].children[i].clone())?;
2526
2527        if !menu_state.inner.with_data(|data| data.open) {
2528            // If the menu is not open, we don't need to show it.
2529            // We also clear the context entity and update the text
2530            // cache so that the item is not bold when the context menu is closed
2531            state.show_context = None;
2532            for key in self.model.order.iter().copied() {
2533                self.update_entity_paragraph(state, key);
2534            }
2535            return None;
2536        }
2537        bounds.x = state.context_cursor.x;
2538        bounds.y = state.context_cursor.y;
2539
2540        Some(
2541            crate::widget::menu::Menu {
2542                tree: menu_state,
2543                menu_roots: std::borrow::Cow::Owned(vec![menu]),
2544                bounds_expand: 16,
2545                menu_overlays_parent: true,
2546                close_condition: CloseCondition {
2547                    leave: false,
2548                    click_outside: true,
2549                    click_inside: true,
2550                },
2551                item_width: ItemWidth::Uniform(240),
2552                item_height: ItemHeight::Dynamic(40),
2553                bar_bounds: bounds,
2554                main_offset: -bounds.height as i32,
2555                cross_offset: 0,
2556                root_bounds_list: vec![bounds],
2557                path_highlight: Some(PathHighlight::MenuActive),
2558                style: std::borrow::Cow::Borrowed(&crate::theme::menu_bar::MenuBarStyle::Default),
2559                position: Point::new(translation.x, translation.y),
2560                is_overlay: true,
2561                window_id: window::Id::NONE,
2562                depth: 0,
2563                on_surface_action: None,
2564            }
2565            .overlay(),
2566        )
2567    }
2568
2569    fn drag_destinations(
2570        &self,
2571        tree: &Tree,
2572        layout: Layout<'_>,
2573        _renderer: &Renderer,
2574        dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
2575    ) {
2576        let local_state = tree.state.downcast_ref::<LocalState>();
2577        let my_id = self.get_drag_id();
2578        let mut pushed = false;
2579
2580        for item in self.variant_bounds(local_state, layout.bounds()) {
2581            if let ItemBounds::Button(_entity, rect) = item {
2582                pushed = true;
2583                log::trace!(
2584                    target: TAB_REORDER_LOG_TARGET,
2585                    "register drag destination id={:?} bounds=({:.2},{:.2},{:.2},{:.2}) mimes={:?}",
2586                    my_id,
2587                    rect.x,
2588                    rect.y,
2589                    rect.width,
2590                    rect.height,
2591                    self.mimes
2592                );
2593                dnd_rectangles.push(DndDestinationRectangle {
2594                    id: my_id,
2595                    rectangle: dnd::Rectangle {
2596                        x: f64::from(rect.x),
2597                        y: f64::from(rect.y),
2598                        width: f64::from(rect.width),
2599                        height: f64::from(rect.height),
2600                    },
2601                    mime_types: self.mimes.clone().into_iter().map(Cow::Owned).collect(),
2602                    actions: DndAction::Copy | DndAction::Move,
2603                    preferred: DndAction::Move,
2604                });
2605            }
2606        }
2607
2608        if let Some(mime) = self.tab_drag.as_ref().map(|d| &d.mime) {
2609            for item in self.variant_bounds(local_state, layout.bounds()) {
2610                if let ItemBounds::Button(_entity, rect) = item {
2611                    pushed = true;
2612                    log::trace!(
2613                        target: TAB_REORDER_LOG_TARGET,
2614                        "register drag destination id={:?} bounds=({:.2},{:.2},{:.2},{:.2}) mimes={:?}",
2615                        my_id,
2616                        rect.x,
2617                        rect.y,
2618                        rect.width,
2619                        rect.height,
2620                        mime
2621                    );
2622                    dnd_rectangles.push(DndDestinationRectangle {
2623                        id: my_id,
2624                        rectangle: dnd::Rectangle {
2625                            x: f64::from(rect.x),
2626                            y: f64::from(rect.y),
2627                            width: f64::from(rect.width),
2628                            height: f64::from(rect.height),
2629                        },
2630                        mime_types: vec![Cow::Owned(mime.clone())],
2631                        actions: DndAction::Copy | DndAction::Move,
2632                        preferred: DndAction::Move,
2633                    });
2634                }
2635            }
2636        }
2637
2638        if !pushed {
2639            let bounds = layout.bounds();
2640            log::trace!(
2641                target: TAB_REORDER_LOG_TARGET,
2642                "register drag destination id={:?} bounds=({:.2},{:.2},{:.2},{:.2}) mimes={:?}",
2643                my_id,
2644                bounds.x,
2645                bounds.y,
2646                bounds.width,
2647                bounds.height,
2648                self.mimes
2649            );
2650            dnd_rectangles.push(DndDestinationRectangle {
2651                id: my_id,
2652                rectangle: dnd::Rectangle {
2653                    x: f64::from(bounds.x),
2654                    y: f64::from(bounds.y),
2655                    width: f64::from(bounds.width),
2656                    height: f64::from(bounds.height),
2657                },
2658                mime_types: self.mimes.clone().into_iter().map(Cow::Owned).collect(),
2659                actions: DndAction::Copy | DndAction::Move,
2660                preferred: DndAction::Move,
2661            });
2662        }
2663    }
2664}
2665
2666impl<'a, Variant, SelectionMode, Message> From<SegmentedButton<'a, Variant, SelectionMode, Message>>
2667    for Element<'a, Message>
2668where
2669    SegmentedButton<'a, Variant, SelectionMode, Message>: SegmentedVariant,
2670    Variant: 'static,
2671    Model<SelectionMode>: Selectable,
2672    SelectionMode: Default,
2673    Message: 'static + Clone,
2674{
2675    fn from(mut widget: SegmentedButton<'a, Variant, SelectionMode, Message>) -> Self {
2676        if widget.model.items.is_empty() {
2677            widget.spacing = 0;
2678        }
2679
2680        Self::new(widget)
2681    }
2682}
2683
2684struct TabDragSource<Message> {
2685    mime: String,
2686    threshold: f32,
2687    _marker: PhantomData<Message>,
2688}
2689
2690impl<Message> TabDragSource<Message> {
2691    fn new(mime: String) -> Self {
2692        Self {
2693            mime,
2694            threshold: 8.0,
2695            _marker: PhantomData,
2696        }
2697    }
2698}
2699
2700struct SimpleDragData {
2701    mime: String,
2702    bytes: Vec<u8>,
2703}
2704
2705impl SimpleDragData {
2706    fn new(mime: String, bytes: Vec<u8>) -> Self {
2707        Self { mime, bytes }
2708    }
2709}
2710
2711impl iced::clipboard::mime::AsMimeTypes for SimpleDragData {
2712    fn available(&self) -> Cow<'static, [String]> {
2713        Cow::Owned(vec![self.mime.clone()])
2714    }
2715
2716    fn as_bytes(&self, mime_type: &str) -> Option<Cow<'static, [u8]>> {
2717        if mime_type == self.mime {
2718            Some(Cow::Owned(self.bytes.clone()))
2719        } else {
2720            None
2721        }
2722    }
2723}
2724
2725#[derive(Clone, Copy)]
2726struct TabDragCandidate {
2727    entity: Entity,
2728    bounds: Rectangle,
2729    origin: Point,
2730}
2731
2732#[derive(Debug, Clone, Copy)]
2733struct Focus {
2734    updated_at: Instant,
2735    now: Instant,
2736}
2737
2738/// State that is maintained by each individual widget.
2739pub struct LocalState {
2740    /// Menu state
2741    pub(crate) menu_state: MenuBarState,
2742    /// Defines how many buttons to show at a time.
2743    pub(super) buttons_visible: usize,
2744    /// Button visibility offset, when collapsed.
2745    pub(super) buttons_offset: usize,
2746    /// Whether buttons need to be collapsed to preserve minimum width
2747    pub(super) collapsed: bool,
2748    /// Visibility of focus state
2749    focused_visible: bool,
2750    /// If the widget is focused or not.
2751    focused: Option<Focus>,
2752    /// The key inside the widget that is currently focused.
2753    focused_item: Item,
2754    /// The ID of the button that is being hovered. Defaults to null.
2755    hovered: Item,
2756    /// The ID of the button that was middle-clicked, but not yet released.
2757    middle_clicked: Option<Item>,
2758    /// Last known length of the model.
2759    pub(super) known_length: usize,
2760    /// Dimensions of internal buttons when shrinking
2761    pub(super) internal_layout: Vec<(Size, Size)>,
2762    /// The paragraphs for each text.
2763    paragraphs: SecondaryMap<Entity, crate::Plain>,
2764    /// Used to detect changes in text.
2765    text_hashes: SecondaryMap<Entity, u64>,
2766    /// Location of cursor when context menu was opened.
2767    context_cursor: Point,
2768    /// Track whether an item is currently showing a context menu.
2769    show_context: Option<Entity>,
2770    /// Time since last tab activation from wheel movements.
2771    wheel_timestamp: Option<Instant>,
2772    /// Dnd state
2773    pub dnd_state: crate::widget::dnd_destination::State<Option<Entity>>,
2774    /// Dnd state
2775    pub offer_mimes: Vec<String>,
2776    /// Tracks multi-touch events
2777    fingers_pressed: HashSet<Finger>,
2778    /// The currently pressed item
2779    pressed_item: Option<Item>,
2780    /// Pending tab drag candidate data
2781    tab_drag_candidate: Option<TabDragCandidate>,
2782    /// Currently dragging tab entity
2783    dragging_tab: Option<Entity>,
2784    /// Current drop hint for drag-and-drop indicator
2785    drop_hint: Option<DropHint>,
2786}
2787
2788#[derive(Clone, Copy, Debug, Default, PartialEq)]
2789enum Item {
2790    NextButton,
2791    #[default]
2792    None,
2793    PrevButton,
2794    Set,
2795    Tab(Entity),
2796}
2797
2798impl LocalState {
2799    fn set_focused(&mut self) {
2800        let now = Instant::now();
2801        LAST_FOCUS_UPDATE.with(|x| x.set(now));
2802
2803        self.focused = Some(Focus {
2804            updated_at: now,
2805            now,
2806        });
2807    }
2808}
2809
2810#[cfg(test)]
2811mod tests {
2812    use super::*;
2813    use crate::widget::segmented_button::{self, Appearance as SegAppearance};
2814    use iced::Size;
2815    use slotmap::SecondaryMap;
2816    use std::collections::HashSet;
2817
2818    #[derive(Clone, Debug)]
2819    enum TestMessage {}
2820
2821    struct TestVariant;
2822
2823    impl<SelectionMode, Message> SegmentedVariant
2824        for SegmentedButton<'_, TestVariant, SelectionMode, Message>
2825    where
2826        Model<SelectionMode>: Selectable,
2827        SelectionMode: Default,
2828        Message: Clone,
2829    {
2830        const VERTICAL: bool = false;
2831
2832        fn variant_appearance(
2833            _theme: &crate::Theme,
2834            _style: &crate::theme::SegmentedButton,
2835        ) -> SegAppearance {
2836            SegAppearance::default()
2837        }
2838
2839        fn variant_bounds<'b>(
2840            &'b self,
2841            _state: &'b LocalState,
2842            bounds: Rectangle,
2843        ) -> Box<dyn Iterator<Item = ItemBounds> + 'b> {
2844            let len = self.model.order.len();
2845            if len == 0 {
2846                return Box::new(std::iter::empty());
2847            }
2848            let width = bounds.width / len as f32;
2849            Box::new(
2850                self.model
2851                    .order
2852                    .iter()
2853                    .copied()
2854                    .enumerate()
2855                    .map(move |(idx, entity)| {
2856                        let rect = Rectangle {
2857                            x: bounds.x + (idx as f32) * width,
2858                            y: bounds.y,
2859                            width,
2860                            height: bounds.height,
2861                        };
2862                        ItemBounds::Button(entity, rect)
2863                    }),
2864            )
2865        }
2866
2867        fn variant_layout(
2868            &self,
2869            _state: &mut LocalState,
2870            _renderer: &crate::Renderer,
2871            _limits: &layout::Limits,
2872        ) -> Size {
2873            Size::ZERO
2874        }
2875    }
2876
2877    fn sample_model() -> (
2878        segmented_button::SingleSelectModel,
2879        Vec<segmented_button::Entity>,
2880    ) {
2881        let mut entities = Vec::new();
2882        let model = segmented_button::Model::builder()
2883            .insert(|b| b.text("One").with_id(|id| entities.push(id)))
2884            .insert(|b| b.text("Two").with_id(|id| entities.push(id)))
2885            .insert(|b| b.text("Three").with_id(|id| entities.push(id)))
2886            .build();
2887        (model, entities)
2888    }
2889
2890    fn test_state(dragging: segmented_button::Entity, len: usize) -> LocalState {
2891        let mut state = LocalState {
2892            menu_state: MenuBarState::default(),
2893            paragraphs: SecondaryMap::new(),
2894            text_hashes: SecondaryMap::new(),
2895            buttons_visible: 0,
2896            buttons_offset: 0,
2897            collapsed: false,
2898            focused: None,
2899            focused_item: Item::default(),
2900            focused_visible: false,
2901            hovered: Item::default(),
2902            known_length: 0,
2903            middle_clicked: None,
2904            internal_layout: Vec::new(),
2905            context_cursor: Point::ORIGIN,
2906            show_context: None,
2907            wheel_timestamp: None,
2908            dnd_state: crate::widget::dnd_destination::State::<Option<Entity>>::new(),
2909            fingers_pressed: HashSet::new(),
2910            pressed_item: None,
2911            tab_drag_candidate: None,
2912            dragging_tab: Some(dragging),
2913            drop_hint: None,
2914            offer_mimes: Vec::new(),
2915        };
2916        state.buttons_visible = len;
2917        state.known_length = len;
2918        state
2919    }
2920
2921    #[test]
2922    fn drop_hint_reports_before_and_after() {
2923        let (model, ids) = sample_model();
2924        let button =
2925            SegmentedButton::<TestVariant, segmented_button::SingleSelect, TestMessage>::new(
2926                &model,
2927            );
2928        let state = test_state(ids[0], model.order.len());
2929        let bounds = Rectangle {
2930            x: 0.0,
2931            y: 0.0,
2932            width: 300.0,
2933            height: 30.0,
2934        };
2935        let before = button
2936            .drop_hint_for_position(&state, bounds, Point::new(10.0, 15.0))
2937            .expect("hint");
2938        assert_eq!(before.entity, ids[0]);
2939        assert!(matches!(before.side, DropSide::Before));
2940
2941        let after = button
2942            .drop_hint_for_position(&state, bounds, Point::new(290.0, 15.0))
2943            .expect("hint");
2944        assert_eq!(after.entity, ids[2]);
2945        assert!(matches!(after.side, DropSide::After));
2946    }
2947}
2948
2949impl operation::Focusable for LocalState {
2950    fn is_focused(&self) -> bool {
2951        self.focused
2952            .is_some_and(|f| f.updated_at == LAST_FOCUS_UPDATE.with(|f| f.get()))
2953    }
2954
2955    fn focus(&mut self) {
2956        self.set_focused();
2957        self.focused_visible = true;
2958        self.focused_item = Item::Set;
2959    }
2960
2961    fn unfocus(&mut self) {
2962        self.focused = None;
2963        self.focused_item = Item::None;
2964        self.focused_visible = false;
2965        self.show_context = None;
2966    }
2967}
2968
2969/// The iced identifier of a segmented button.
2970#[derive(Debug, Clone, PartialEq)]
2971pub struct Id(widget::Id);
2972
2973impl Id {
2974    /// Creates a custom [`Id`].
2975    pub fn new(id: impl Into<std::borrow::Cow<'static, str>>) -> Self {
2976        Self(widget::Id::new(id))
2977    }
2978
2979    /// Creates a unique [`Id`].
2980    ///
2981    /// This function produces a different [`Id`] every time it is called.
2982    #[must_use]
2983    #[inline]
2984    pub fn unique() -> Self {
2985        Self(widget::Id::unique())
2986    }
2987}
2988
2989impl From<Id> for widget::Id {
2990    fn from(id: Id) -> Self {
2991        id.0
2992    }
2993}
2994
2995/// Calculates the bounds of the close button within the area of an item.
2996fn close_bounds(area: Rectangle<f32>, icon_size: f32) -> Rectangle<f32> {
2997    Rectangle {
2998        x: area.x + area.width - icon_size - 8.0,
2999        y: area.center_y() - (icon_size / 2.0),
3000        width: icon_size,
3001        height: icon_size,
3002    }
3003}
3004
3005/// Calculate the bounds of the `next_tab` button.
3006fn next_tab_bounds(bounds: &Rectangle, button_height: f32) -> Rectangle {
3007    Rectangle {
3008        x: bounds.x + bounds.width - button_height,
3009        y: bounds.y,
3010        width: button_height,
3011        height: button_height,
3012    }
3013}
3014
3015/// Calculate the bounds of the `prev_tab` button.
3016fn prev_tab_bounds(bounds: &Rectangle, button_height: f32) -> Rectangle {
3017    Rectangle {
3018        x: bounds.x,
3019        y: bounds.y,
3020        width: button_height,
3021        height: button_height,
3022    }
3023}
3024
3025#[allow(clippy::too_many_arguments)]
3026fn draw_icon<Message: 'static>(
3027    renderer: &mut Renderer,
3028    theme: &crate::Theme,
3029    style: &renderer::Style,
3030    cursor: mouse::Cursor,
3031    viewport: &Rectangle,
3032    color: Color,
3033    bounds: Rectangle,
3034    icon: Icon,
3035) {
3036    let layout_node = layout::Node::new(Size {
3037        width: bounds.width,
3038        height: bounds.width,
3039    })
3040    .move_to(Point {
3041        x: bounds.x,
3042        y: bounds.y,
3043    });
3044
3045    Widget::<Message, crate::Theme, Renderer>::draw(
3046        Element::<Message>::from(icon).as_widget(),
3047        &Tree::empty(),
3048        renderer,
3049        theme,
3050        &renderer::Style {
3051            icon_color: color,
3052            text_color: color,
3053            scale_factor: style.scale_factor,
3054        },
3055        Layout::new(&layout_node),
3056        cursor,
3057        viewport,
3058    );
3059}
3060
3061fn draw_drop_indicator(
3062    renderer: &mut Renderer,
3063    bounds: Rectangle,
3064    side: DropSide,
3065    vertical: bool,
3066    color: Color,
3067) {
3068    let thickness = 4.0;
3069    let quad_bounds = if vertical {
3070        let y = match side {
3071            DropSide::Before => bounds.y - thickness / 2.0,
3072            DropSide::After => bounds.y + bounds.height - thickness / 2.0,
3073        };
3074
3075        Rectangle {
3076            x: bounds.x,
3077            y,
3078            width: bounds.width,
3079            height: thickness,
3080        }
3081    } else {
3082        let x = match side {
3083            DropSide::Before => bounds.x - thickness / 2.0,
3084            DropSide::After => bounds.x + bounds.width - thickness / 2.0,
3085        };
3086
3087        Rectangle {
3088            x,
3089            y: bounds.y,
3090            width: thickness,
3091            height: bounds.height,
3092        }
3093    };
3094
3095    renderer.fill_quad(
3096        renderer::Quad {
3097            bounds: quad_bounds,
3098            border: Border {
3099                radius: 2.0.into(),
3100                ..Default::default()
3101            },
3102            shadow: Shadow::default(),
3103            snap: true,
3104        },
3105        Background::Color(color),
3106    );
3107}
3108
3109fn left_button_released(event: &Event) -> bool {
3110    matches!(
3111        event,
3112        Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left,))
3113    )
3114}
3115
3116fn right_button_released(event: &Event) -> bool {
3117    matches!(
3118        event,
3119        Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right,))
3120    )
3121}
3122
3123fn is_pressed(event: &Event) -> bool {
3124    matches!(
3125        event,
3126        Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
3127            | Event::Touch(touch::Event::FingerPressed { .. })
3128    )
3129}
3130
3131fn is_lifted(event: &Event) -> bool {
3132    matches!(
3133        event,
3134        Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left,))
3135            | Event::Touch(touch::Event::FingerLifted { .. })
3136    )
3137}
3138
3139fn touch_lifted(event: &Event) -> bool {
3140    matches!(event, Event::Touch(touch::Event::FingerLifted { .. }))
3141}