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 + 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 + 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                    id
942                } else {
943                    window::Id::unique()
944                }
945            });
946            let Some(entity) = state.show_context else {
947                return;
948            };
949
950            let Some((mut bounds, i)) = self
951                .variant_bounds(state, layout.bounds())
952                .filter_map(|item| match item {
953                    ItemBounds::Button(entity, bounds) => Some((bounds, entity)),
954                    _ => None,
955                })
956                .enumerate()
957                .find_map(|(i, (bounds, e))| if e == entity { Some((bounds, i)) } else { None })
958            else {
959                return;
960            };
961
962            assert!(
963                self.context_menu
964                    .as_ref()
965                    .is_none_or(|m| m[0].children.len() == self.model.len()),
966                "model length must match the number of context menus"
967            );
968            let menu = self
969                .context_menu
970                .as_mut()
971                .map(|m| m[0].children[i].clone())
972                .unwrap();
973
974            bounds.x = state.context_cursor.x;
975            bounds.y = state.context_cursor.y;
976
977            let mut popup_menu: menu::Menu<'static, _> = menu::Menu {
978                tree: my_state.clone(),
979                menu_roots: std::borrow::Cow::Owned(vec![menu]),
980                bounds_expand: 0,
981                menu_overlays_parent: false,
982                close_condition: CloseCondition {
983                    leave: false,
984                    click_outside: true,
985                    click_inside: true,
986                },
987                item_width: ItemWidth::Uniform(240),
988                item_height: ItemHeight::Dynamic(40),
989                bar_bounds: bounds,
990                main_offset: 0,
991                cross_offset: 0,
992                root_bounds_list: vec![bounds],
993                path_highlight: Some(PathHighlight::MenuActive),
994                style: std::borrow::Cow::Borrowed(&crate::theme::menu_bar::MenuBarStyle::Default),
995                position: Point::new(0., 0.),
996                is_overlay: false,
997                window_id: id,
998                depth: 0,
999                on_surface_action: self.on_surface_action.clone(),
1000            };
1001
1002            menu::init_root_menu(
1003                &mut popup_menu,
1004                renderer,
1005                shell,
1006                view_cursor.position().unwrap(),
1007                viewport.size(),
1008                Vector::new(0., 0.),
1009                bounds,
1010                0., // TODO offset?
1011            );
1012            let (anchor_rect, gravity) = my_state.inner.with_data_mut(|state| {
1013                state.popup_id.insert(self.window_id, id);
1014                (state
1015                    .menu_states
1016                    .iter()
1017                    .find(|s| s.index.is_none())
1018                    .map(|s| s.menu_bounds.parent_bounds)
1019                    .map_or_else(
1020                        || {
1021                            let bounds = layout.bounds();
1022                            Rectangle {
1023                                x: bounds.x as i32,
1024                                y: bounds.y as i32,
1025                                width: 1,
1026                                height: 1,
1027                            }
1028                        },
1029                        |r| Rectangle {
1030                            x: r.x as i32,
1031                            y: r.y as i32,
1032                            width: 1,
1033                            height: 1,
1034                        },
1035                    ), match (state.horizontal_direction, state.vertical_direction) {
1036                        (menu::Direction::Positive, menu::Direction::Positive) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight,
1037                        (menu::Direction::Positive, menu::Direction::Negative) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::TopRight,
1038                        (menu::Direction::Negative, menu::Direction::Positive) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomLeft,
1039                        (menu::Direction::Negative, menu::Direction::Negative) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::TopLeft,
1040                    })
1041            });
1042
1043            let menu_node =
1044                popup_menu.layout(renderer, layout::Limits::NONE.min_width(1.).min_height(1.));
1045            let popup_size = menu_node.size();
1046            let positioner = SctkPositioner {
1047                size: Some((
1048                    popup_size.width.ceil() as u32 + 2,
1049                    popup_size.height.ceil() as u32 + 2,
1050                )),
1051                anchor_rect,
1052                anchor:
1053                    cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Anchor::BottomLeft,
1054                gravity,
1055                reactive: true,
1056                ..Default::default()
1057            };
1058            let parent = self.window_id;
1059
1060            let t = THEME.lock().unwrap();
1061            let styling = t.appearance(&crate::theme::menu_bar::MenuBarStyle::Default, false);
1062            drop(t);
1063            let rad = styling.menu_border_radius;
1064
1065            /// Used to create a popup message from within a widget.
1066            #[cfg(wayland_platform)]
1067            #[must_use]
1068            pub fn simple_popup<Message: 'static>(
1069                live_settings: impl Fn() -> LiveSettings + Send + Sync + 'static,
1070                settings: impl Fn()
1071                    -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings
1072                + Send
1073                + Sync
1074                + 'static,
1075                view: Option<impl Fn() -> crate::Element<'static, Message> + Send + Sync + 'static>,
1076            ) -> crate::surface::Action {
1077                use std::any::Any;
1078
1079                let boxed: Box<
1080                    dyn Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings
1081                        + Send
1082                        + Sync
1083                        + 'static,
1084                > = Box::new(settings);
1085                let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed);
1086
1087                let boxed_live: Box<dyn Fn() -> LiveSettings + Send + Sync + 'static> =
1088                    Box::new(live_settings);
1089                let boxed_live: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed_live);
1090
1091                crate::surface::Action::Popup(
1092                    Arc::new(boxed),
1093                    Arc::new(boxed_live),
1094                    view.map(|view| {
1095                        let boxed: Box<
1096                            dyn Fn() -> crate::Element<'static, Message> + Send + Sync + 'static,
1097                        > = Box::new(view);
1098                        let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed);
1099                        Arc::new(boxed)
1100                    }),
1101                )
1102            }
1103            shell.publish((surface_action)(simple_popup(
1104                move || LiveSettings {
1105                    corners: Some(CornerRadius {
1106                        top_left: rad[0] as u32,
1107                        top_right: rad[1] as u32,
1108                        bottom_left: rad[2] as u32,
1109                        bottom_right: rad[3] as u32,
1110                    }),
1111                    ..Default::default()
1112                },
1113                move || SctkPopupSettings {
1114                    parent,
1115                    id,
1116                    positioner: positioner.clone(),
1117                    parent_size: None,
1118                    grab: true,
1119                    close_with_children: false,
1120                    input_zone: None,
1121                },
1122                Some(move || {
1123                    Element::from(crate::widget::container(popup_menu.clone()).center(Length::Fill))
1124                }),
1125            )));
1126        }
1127    }
1128}
1129
1130impl<Variant, SelectionMode, Message> Widget<Message, crate::Theme, Renderer>
1131    for SegmentedButton<'_, Variant, SelectionMode, Message>
1132where
1133    Self: SegmentedVariant,
1134    Model<SelectionMode>: Selectable,
1135    SelectionMode: Default,
1136    Message: 'static + Clone,
1137{
1138    fn id(&self) -> Option<widget::Id> {
1139        Some(self.id.0.clone())
1140    }
1141
1142    fn set_id(&mut self, id: widget::Id) {
1143        self.id = Id(id);
1144    }
1145
1146    fn children(&self) -> Vec<Tree> {
1147        let mut children = Vec::new();
1148
1149        // Assign the context menu's elements as this widget's children.
1150        if let Some(ref context_menu) = self.context_menu {
1151            let mut tree = Tree::empty();
1152            tree.state = tree::State::new(MenuBarState::default());
1153            tree.children = menu_roots_children(context_menu);
1154            children.push(tree);
1155        }
1156
1157        children
1158    }
1159
1160    fn tag(&self) -> tree::Tag {
1161        tree::Tag::of::<LocalState>()
1162    }
1163
1164    fn state(&self) -> tree::State {
1165        #[allow(clippy::default_trait_access)]
1166        tree::State::new(LocalState {
1167            menu_state: Default::default(),
1168            paragraphs: SecondaryMap::new(),
1169            text_hashes: SecondaryMap::new(),
1170            buttons_visible: Default::default(),
1171            buttons_offset: Default::default(),
1172            collapsed: Default::default(),
1173            focused: Default::default(),
1174            focused_item: Default::default(),
1175            focused_visible: false,
1176            hovered: Default::default(),
1177            known_length: Default::default(),
1178            middle_clicked: Default::default(),
1179            internal_layout: Default::default(),
1180            context_cursor: Point::default(),
1181            show_context: Default::default(),
1182            wheel_timestamp: Default::default(),
1183            dnd_state: Default::default(),
1184            fingers_pressed: Default::default(),
1185            pressed_item: None,
1186            tab_drag_candidate: None,
1187            dragging_tab: None,
1188            drop_hint: None,
1189            offer_mimes: Vec::new(),
1190        })
1191    }
1192
1193    fn diff(&mut self, tree: &mut Tree) {
1194        let state = tree.state.downcast_mut::<LocalState>();
1195        for key in self.model.order.iter().copied() {
1196            self.update_entity_paragraph(state, key);
1197        }
1198
1199        // Diff the context menu
1200        if let Some(context_menu) = &mut self.context_menu {
1201            state.menu_state.inner.with_data_mut(|inner| {
1202                menu_roots_diff(context_menu, &mut inner.tree);
1203            });
1204        }
1205
1206        // Unfocus if another segmented control was focused.
1207        if let Some(f) = state.focused.as_ref()
1208            && f.updated_at != LAST_FOCUS_UPDATE.with(|f| f.get())
1209        {
1210            state.unfocus();
1211        }
1212    }
1213
1214    fn size(&self) -> Size<Length> {
1215        Size::new(self.width, self.height)
1216    }
1217
1218    fn layout(
1219        &mut self,
1220        tree: &mut Tree,
1221        renderer: &Renderer,
1222        limits: &layout::Limits,
1223    ) -> layout::Node {
1224        let state = tree.state.downcast_mut::<LocalState>();
1225        let limits = limits.shrink(self.padding);
1226        let size = self
1227            .variant_layout(state, renderer, &limits)
1228            .expand(self.padding);
1229        layout::Node::new(size)
1230    }
1231
1232    #[allow(clippy::too_many_lines)]
1233    fn update(
1234        &mut self,
1235        tree: &mut Tree,
1236        mut event: &Event,
1237        layout: Layout<'_>,
1238        cursor_position: mouse::Cursor,
1239        renderer: &Renderer,
1240        clipboard: &mut dyn Clipboard,
1241        shell: &mut Shell<'_, Message>,
1242        viewport: &iced::Rectangle,
1243    ) {
1244        let my_bounds = layout.bounds();
1245        let state = tree.state.downcast_mut::<LocalState>();
1246
1247        let my_id = self.get_drag_id();
1248
1249        if let Event::Dnd(e) = &mut event {
1250            let entity = state
1251                .dnd_state
1252                .drag_offer
1253                .as_ref()
1254                .map(|dnd_state| dnd_state.data);
1255            log::trace!(
1256                target: TAB_REORDER_LOG_TARGET,
1257                "segmented button {:?} received DnD event: {:?} entity={entity:?}",
1258                my_id,
1259                e
1260            );
1261            match e {
1262                DndEvent::Source(SourceEvent::Cancelled | SourceEvent::Finished) => {
1263                    if state.dragging_tab.take().is_some() {
1264                        state.tab_drag_candidate = None;
1265                        state.drop_hint = None;
1266                        self.emit_drop_hint(shell, state.drop_hint);
1267                        log::trace!(
1268                            target: TAB_REORDER_LOG_TARGET,
1269                            "tab drag source finished id={:?}",
1270                            my_id
1271                        );
1272                        shell.capture_event();
1273                        return;
1274                    }
1275                }
1276                DndEvent::Offer(
1277                    id,
1278                    OfferEvent::Enter {
1279                        x, y, mime_types, ..
1280                    },
1281                ) if Some(my_id) == *id => {
1282                    let entity = self
1283                        .variant_bounds(state, my_bounds)
1284                        .filter_map(|item| match item {
1285                            ItemBounds::Button(entity, bounds) => Some((entity, bounds)),
1286                            _ => None,
1287                        })
1288                        .find(|(_key, bounds)| bounds.contains(Point::new(*x as f32, *y as f32)))
1289                        .map(|(key, _)| key);
1290                    state.drop_hint = self.drop_hint_for_position(
1291                        state,
1292                        my_bounds,
1293                        Point::new(*x as f32, *y as f32),
1294                    );
1295                    self.emit_drop_hint(shell, state.drop_hint);
1296                    log::trace!(
1297                        target: TAB_REORDER_LOG_TARGET,
1298                        "offer enter id={my_id:?} entity={entity:?} @ ({x},{y}) mimes={mime_types:?}"
1299                    );
1300                    // force hovered state update
1301                    if let Some(entity) = entity {
1302                        state.hovered = Item::Tab(entity);
1303                        for key in self.model.order.iter().copied() {
1304                            self.update_entity_paragraph(state, key);
1305                        }
1306                    }
1307
1308                    let on_dnd_enter = self
1309                        .on_dnd_enter
1310                        .as_ref()
1311                        .zip(entity)
1312                        .map(|(on_enter, entity)| move |_, _, mimes| on_enter(entity, mimes));
1313                    let mimes = if let Some(mime) = self.tab_drag.as_ref().map(|d| &d.mime)
1314                        && mime_types.is_empty()
1315                    {
1316                        vec![mime.clone()]
1317                    } else {
1318                        mime_types.clone()
1319                    };
1320                    state.offer_mimes.clone_from(&mimes);
1321
1322                    _ = state
1323                        .dnd_state
1324                        .on_enter::<Message>(*x, *y, mimes, on_dnd_enter, entity);
1325                }
1326                DndEvent::Offer(id, OfferEvent::LeaveDestination) if Some(my_id) != *id => {}
1327                DndEvent::Offer(id, leave)
1328                    if matches!(leave, OfferEvent::Leave | OfferEvent::LeaveDestination)
1329                        && Some(my_id) == *id =>
1330                {
1331                    state.drop_hint = None;
1332                    self.emit_drop_hint(shell, state.drop_hint);
1333                    if let Some(Some(entity)) = entity {
1334                        if let Some(on_dnd_leave) = self.on_dnd_leave.as_ref() {
1335                            shell.publish(on_dnd_leave(entity));
1336                        }
1337                    }
1338                    log::trace!(
1339                        target: TAB_REORDER_LOG_TARGET,
1340                        "offer leave id={my_id:?} entity={entity:?}"
1341                    );
1342                    state.hovered = Item::None;
1343                    for key in self.model.order.iter().copied() {
1344                        self.update_entity_paragraph(state, key);
1345                    }
1346                    _ = state.dnd_state.on_leave::<Message>(None);
1347                }
1348                DndEvent::Offer(id, OfferEvent::Motion { x, y }) if Some(my_id) == *id => {
1349                    log::trace!(
1350                        target: TAB_REORDER_LOG_TARGET,
1351                        "offer motion id={my_id:?} cursor=({x},{y}) current_entity={entity:?}"
1352                    );
1353                    let new = self
1354                        .variant_bounds(state, my_bounds)
1355                        .filter_map(|item| match item {
1356                            ItemBounds::Button(entity, bounds) => Some((entity, bounds)),
1357                            _ => None,
1358                        })
1359                        .find(|(_key, bounds)| bounds.contains(Point::new(*x as f32, *y as f32)))
1360                        .map(|(key, _)| key);
1361                    if let Some(new_entity) = new {
1362                        state.dnd_state.on_motion::<Message>(
1363                            *x,
1364                            *y,
1365                            None::<fn(_, _) -> Message>,
1366                            None::<fn(_, _, _) -> Message>,
1367                            Some(new_entity),
1368                        );
1369                        state.drop_hint = self.drop_hint_for_position(
1370                            state,
1371                            my_bounds,
1372                            Point::new(*x as f32, *y as f32),
1373                        );
1374                        self.emit_drop_hint(shell, state.drop_hint);
1375                        if Some(Some(new_entity)) != entity {
1376                            state.hovered = Item::Tab(new_entity);
1377                            for key in self.model.order.iter().copied() {
1378                                self.update_entity_paragraph(state, key);
1379                            }
1380                            let prev_action = state
1381                                .dnd_state
1382                                .drag_offer
1383                                .as_ref()
1384                                .map(|dnd| dnd.selected_action);
1385                            if let Some(on_dnd_enter) = self.on_dnd_enter.as_ref() {
1386                                shell.publish(on_dnd_enter(new_entity, state.offer_mimes.clone()));
1387                            }
1388                            if let Some(dnd) = state.dnd_state.drag_offer.as_mut() {
1389                                dnd.data = Some(new_entity);
1390                                if let Some(prev_action) = prev_action {
1391                                    dnd.selected_action = prev_action;
1392                                }
1393                            }
1394                        }
1395                    } else if entity.is_some() {
1396                        state.hovered = Item::None;
1397                        for key in self.model.order.iter().copied() {
1398                            self.update_entity_paragraph(state, key);
1399                        }
1400                        log::trace!(
1401                            target: TAB_REORDER_LOG_TARGET,
1402                            "offer motion leaving id={my_id:?}"
1403                        );
1404                        state.drop_hint = None;
1405                        self.emit_drop_hint(shell, state.drop_hint);
1406                        state.dnd_state.on_motion::<Message>(
1407                            *x,
1408                            *y,
1409                            None::<fn(_, _) -> Message>,
1410                            None::<fn(_, _, _) -> Message>,
1411                            None,
1412                        );
1413                        if let Some(on_dnd_leave) = self.on_dnd_leave.as_ref() {
1414                            if let Some(Some(entity)) = entity {
1415                                shell.publish(on_dnd_leave(entity));
1416                            }
1417                        }
1418                    }
1419                }
1420                DndEvent::Offer(id, OfferEvent::Drop) if Some(my_id) == *id => {
1421                    log::trace!(
1422                        target: TAB_REORDER_LOG_TARGET,
1423                        "offer drop id={my_id:?} entity={entity:?}"
1424                    );
1425                    _ = state
1426                        .dnd_state
1427                        .on_drop::<Message>(None::<fn(_, _) -> Message>);
1428                }
1429                DndEvent::Offer(id, OfferEvent::SelectedAction(action)) if Some(my_id) == *id => {
1430                    if state.dnd_state.drag_offer.is_some() {
1431                        log::trace!(
1432                            target: TAB_REORDER_LOG_TARGET,
1433                            "offer selected action id={my_id:?} action={action:?} entity={entity:?}"
1434                        );
1435                        _ = state
1436                            .dnd_state
1437                            .on_action_selected::<Message>(*action, None::<fn(_) -> Message>);
1438                    }
1439                }
1440                DndEvent::Offer(id, OfferEvent::Data { data, mime_type }) if Some(my_id) == *id => {
1441                    log::trace!(
1442                        target: TAB_REORDER_LOG_TARGET,
1443                        "offer data id={my_id:?} entity={entity:?} mime={mime_type:?}"
1444                    );
1445                    let drop_entity = entity
1446                        .flatten()
1447                        .or_else(|| state.drop_hint.map(|hint| hint.entity));
1448                    let allow_reorder = state
1449                        .dnd_state
1450                        .drag_offer
1451                        .as_ref()
1452                        .is_some_and(|offer| offer.selected_action.contains(DndAction::Move));
1453                    let pending_reorder = if allow_reorder
1454                        && self.on_reorder.is_some()
1455                        && self.tab_drag.as_ref().is_some_and(|d| d.mime == *mime_type)
1456                        && state.dragging_tab.is_some()
1457                    {
1458                        drop_entity.and_then(|target| self.reorder_event_for_drop(state, target))
1459                    } else {
1460                        None
1461                    };
1462                    if let Some(entity) = drop_entity {
1463                        let on_drop = self.on_dnd_drop.as_ref();
1464                        let on_drop = on_drop.map(|on_drop| {
1465                            |mime, data, action, _, _| on_drop(entity, data, mime, action)
1466                        });
1467
1468                        let (maybe_msg, ret) = state.dnd_state.on_data_received(
1469                            mime_type.clone(),
1470                            data.clone(),
1471                            None::<fn(_, _) -> Message>,
1472                            on_drop,
1473                        );
1474                        if matches!(ret, iced::event::Status::Captured) {
1475                            shell.capture_event();
1476                        }
1477                        if let Some(msg) = maybe_msg {
1478                            log::trace!(
1479                                target: TAB_REORDER_LOG_TARGET,
1480                                "publishing drop message entity={entity:?}"
1481                            );
1482                            shell.publish(msg);
1483                        }
1484                        state.drop_hint = None;
1485
1486                        self.emit_drop_hint(shell, state.drop_hint);
1487                        if let Some(event) = pending_reorder {
1488                            state.focused_item = Item::Tab(event.dragged);
1489                            state.hovered = Item::None;
1490                            for key in self.model.order.iter().copied() {
1491                                self.update_entity_paragraph(state, key);
1492                            }
1493                            if let Some(on_reorder) = self.on_reorder.as_ref() {
1494                                shell.publish(on_reorder(event));
1495                                shell.capture_event();
1496                                return;
1497                            }
1498                        }
1499                        return;
1500                    }
1501                }
1502                _ => {}
1503            }
1504        }
1505
1506        if cursor_position.is_over(my_bounds) {
1507            let fingers_pressed = state.fingers_pressed.len();
1508
1509            match event {
1510                Event::Touch(touch::Event::FingerPressed { id, .. }) => {
1511                    state.fingers_pressed.insert(*id);
1512                }
1513
1514                Event::Touch(touch::Event::FingerLifted { id, .. }) => {
1515                    state.fingers_pressed.remove(id);
1516                }
1517                _ => (),
1518            }
1519
1520            // Check for clicks on the previous and next tab buttons, when tabs are collapsed.
1521            if state.collapsed {
1522                // Check if the prev tab button was clicked.
1523                if cursor_position
1524                    .is_over(prev_tab_bounds(&my_bounds, f32::from(self.button_height)))
1525                    && self.prev_tab_sensitive(state)
1526                {
1527                    state.hovered = Item::PrevButton;
1528                    for key in self.model.order.iter().copied() {
1529                        self.update_entity_paragraph(state, key);
1530                    }
1531                    if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
1532                    | Event::Touch(touch::Event::FingerLifted { .. }) = event
1533                    {
1534                        state.buttons_offset -= 1;
1535                    }
1536                } else {
1537                    // Check if the next tab button was clicked.
1538                    if cursor_position
1539                        .is_over(next_tab_bounds(&my_bounds, f32::from(self.button_height)))
1540                        && self.next_tab_sensitive(state)
1541                    {
1542                        state.hovered = Item::NextButton;
1543                        for key in self.model.order.iter().copied() {
1544                            self.update_entity_paragraph(state, key);
1545                        }
1546                        if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
1547                        | Event::Touch(touch::Event::FingerLifted { .. }) = event
1548                        {
1549                            state.buttons_offset += 1;
1550                        }
1551                    }
1552                }
1553            }
1554
1555            for (key, bounds) in self
1556                .variant_bounds(state, my_bounds)
1557                .filter_map(|item| match item {
1558                    ItemBounds::Button(entity, bounds) => Some((entity, bounds)),
1559                    _ => None,
1560                })
1561                .collect::<Vec<_>>()
1562            {
1563                if cursor_position.is_over(bounds) {
1564                    if self.model.items[key].enabled {
1565                        // Record that the mouse is hovering over this button.
1566                        if state.hovered != Item::Tab(key) {
1567                            state.hovered = Item::Tab(key);
1568                            for key in self.model.order.iter().copied() {
1569                                self.update_entity_paragraph(state, key);
1570                            }
1571                        }
1572
1573                        let close_button_bounds =
1574                            close_bounds(bounds, f32::from(self.close_icon.size));
1575                        let over_close_button = self.model.items[key].closable
1576                            && cursor_position.is_over(close_button_bounds);
1577
1578                        // If marked as closable, show a close icon.
1579                        if self.model.items[key].closable {
1580                            // Emit close message if the close button is pressed.
1581                            if let Some(on_close) = self.on_close.as_ref() {
1582                                if over_close_button
1583                                    && (left_button_released(&event)
1584                                        || (touch_lifted(&event) && fingers_pressed == 1))
1585                                {
1586                                    shell.publish(on_close(key));
1587                                    shell.capture_event();
1588                                    return;
1589                                }
1590
1591                                if self.on_middle_press.is_none() {
1592                                    // Emit close message if the tab is middle clicked.
1593                                    if let Event::Mouse(mouse::Event::ButtonReleased(
1594                                        mouse::Button::Middle,
1595                                    )) = event
1596                                    {
1597                                        if state.middle_clicked == Some(Item::Tab(key)) {
1598                                            shell.publish(on_close(key));
1599                                            shell.capture_event();
1600                                            return;
1601                                        }
1602
1603                                        state.middle_clicked = None;
1604                                    }
1605                                }
1606                            }
1607                        }
1608
1609                        if self.tab_drag.is_some()
1610                            && matches!(
1611                                event,
1612                                Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
1613                            )
1614                            && !over_close_button
1615                            && let Some(position) = cursor_position.position()
1616                        {
1617                            state.tab_drag_candidate = Some(TabDragCandidate {
1618                                entity: key,
1619                                bounds,
1620                                origin: position,
1621                            });
1622                            if let Some(tab_drag) = self.tab_drag.as_ref() {
1623                                log::trace!(
1624                                    target: TAB_REORDER_LOG_TARGET,
1625                                    "tab drag candidate entity={:?} origin=({:.2},{:.2}) bounds=({:.2},{:.2},{:.2},{:.2}) threshold={}",
1626                                    key,
1627                                    position.x,
1628                                    position.y,
1629                                    bounds.x,
1630                                    bounds.y,
1631                                    bounds.width,
1632                                    bounds.height,
1633                                    tab_drag.threshold
1634                                );
1635                            }
1636                        }
1637
1638                        if is_lifted(&event) {
1639                            state.unfocus();
1640                        }
1641
1642                        #[cfg(wayland_platform)]
1643                        if is_pressed(event)
1644                            && let Some(on_context) = self.on_context.as_ref()
1645                        {
1646                            let (was_open, id) = state.menu_state.inner.with_data_mut(|data| {
1647                                let was_open = data.open;
1648                                data.reset();
1649                                data.open = false;
1650                                data.view_cursor = cursor_position;
1651                                let root = data.popup_id.remove(&self.window_id);
1652                                data.popup_id.clear();
1653                                (was_open, root)
1654                            });
1655                            if let Some(w) = id
1656                                && let Some(surface_action) = self.on_surface_action.as_ref()
1657                                && was_open
1658                            {
1659                                use crate::surface::action::destroy_popup;
1660
1661                                shell.publish((surface_action)(destroy_popup(w)));
1662                                return;
1663                            }
1664                        }
1665
1666                        if let Some(on_activate) = self.on_activate.as_ref() {
1667                            if is_pressed(event) {
1668                                state.pressed_item = Some(Item::Tab(key));
1669                            } else if is_lifted(&event) && self.button_is_pressed(state, key) {
1670                                shell.publish(on_activate(key));
1671                                state.set_focused();
1672                                state.focused_item = Item::Tab(key);
1673                                state.pressed_item = None;
1674                                shell.capture_event();
1675                                return;
1676                            }
1677                        }
1678
1679                        // Present a context menu on a right click event.
1680                        if self.context_menu.is_some()
1681                            && let Some(on_context) = self.on_context.as_ref()
1682                            && (right_button_released(&event)
1683                                || (touch_lifted(&event) && fingers_pressed == 2))
1684                        {
1685                            state.show_context = Some(key);
1686                            state.context_cursor = cursor_position.position().unwrap_or_default();
1687
1688                            state.menu_state.inner.with_data_mut(|data| {
1689                                // Clear stale MenuBounds from any previous context menu before opening a new one.
1690                                data.reset();
1691                                data.open = true;
1692                                data.view_cursor = cursor_position;
1693                            });
1694
1695                            shell.publish(on_context(key));
1696                            shell.capture_event();
1697
1698                            #[cfg(wayland_platform)]
1699                            if matches!(
1700                                crate::app::cosmic::WINDOWING_SYSTEM.get(),
1701                                Some(crate::app::cosmic::WindowingSystem::Wayland)
1702                            ) {
1703                                self.create_popup(
1704                                    layout,
1705                                    cursor_position,
1706                                    renderer,
1707                                    shell,
1708                                    viewport,
1709                                    tree,
1710                                );
1711                            }
1712                            return;
1713                        }
1714                        if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) =
1715                            event
1716                        {
1717                            state.middle_clicked = Some(Item::Tab(key));
1718                            if let Some(on_middle_press) = self.on_middle_press.as_ref() {
1719                                shell.publish(on_middle_press(key));
1720                                shell.capture_event();
1721                                return;
1722                            }
1723                        }
1724                    }
1725
1726                    break;
1727                } else if state.hovered == Item::Tab(key) {
1728                    state.hovered = Item::None;
1729                    self.update_entity_paragraph(state, key);
1730                }
1731            }
1732
1733            if self.scrollable_focus
1734                && let Some(on_activate) = self.on_activate.as_ref()
1735                && let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event
1736            {
1737                let current = Instant::now();
1738
1739                // Permit successive scroll wheel events only after a given delay.
1740                if state.wheel_timestamp.is_none_or(|previous| {
1741                    current.duration_since(previous) > Duration::from_millis(250)
1742                }) {
1743                    state.wheel_timestamp = Some(current);
1744
1745                    match delta {
1746                        ScrollDelta::Lines { y, .. } | ScrollDelta::Pixels { y, .. } => {
1747                            let mut activate_key = None;
1748
1749                            if *y < 0.0 {
1750                                let mut prev_key = Entity::null();
1751
1752                                for key in self.model.order.iter().copied() {
1753                                    if self.model.is_active(key) && !prev_key.is_null() {
1754                                        activate_key = Some(prev_key);
1755                                    }
1756
1757                                    if self.model.is_enabled(key) {
1758                                        prev_key = key;
1759                                    }
1760                                }
1761                            } else if *y > 0.0 {
1762                                let mut buttons = self.model.order.iter().copied();
1763                                while let Some(key) = buttons.next() {
1764                                    if self.model.is_active(key) {
1765                                        for key in buttons {
1766                                            if self.model.is_enabled(key) {
1767                                                activate_key = Some(key);
1768                                                break;
1769                                            }
1770                                        }
1771                                        break;
1772                                    }
1773                                }
1774                            }
1775
1776                            if let Some(key) = activate_key {
1777                                shell.publish(on_activate(key));
1778                                state.set_focused();
1779                                state.focused_item = Item::Tab(key);
1780                                shell.capture_event();
1781                                return;
1782                            }
1783                        }
1784                    }
1785                }
1786            }
1787        } else {
1788            if let Item::Tab(_key) = std::mem::replace(&mut state.hovered, Item::None) {
1789                for key in self.model.order.iter().copied() {
1790                    self.update_entity_paragraph(state, key);
1791                }
1792            }
1793            if state.is_focused() {
1794                // Unfocus on clicks outside of the boundaries of the segmented button.
1795                if is_pressed(&event) {
1796                    state.unfocus();
1797                    state.pressed_item = None;
1798                    return;
1799                }
1800            } else if is_lifted(&event) {
1801                state.pressed_item = None;
1802            }
1803        }
1804
1805        if let (Some(tab_drag), Some(candidate)) =
1806            (self.tab_drag.as_ref(), state.tab_drag_candidate)
1807            && let Event::Mouse(mouse::Event::CursorMoved { .. }) = event
1808            && let Some(position) = cursor_position.position()
1809            && position.distance(candidate.origin) >= tab_drag.threshold
1810            && let Some(candidate) = state.tab_drag_candidate.take()
1811        {
1812            log::trace!(
1813                target: TAB_REORDER_LOG_TARGET,
1814                "tab drag threshold met entity={:?} distance={:.2} threshold={}",
1815                candidate.entity,
1816                position.distance(candidate.origin),
1817                tab_drag.threshold
1818            );
1819            if self.start_tab_drag(
1820                state,
1821                candidate.entity,
1822                candidate.bounds,
1823                position,
1824                clipboard,
1825            ) {
1826                shell.capture_event();
1827                return;
1828            }
1829        }
1830
1831        if (matches!(event, Event::Mouse(mouse::Event::ButtonReleased(_))) || (touch_lifted(event)))
1832            && let Some(_id) = state
1833                .menu_state
1834                .inner
1835                .with_data_mut(|ms| ms.popup_id.remove(&self.window_id))
1836        {
1837            #[cfg(wayland_platform)]
1838            {
1839                let surface_action = self.on_surface_action.as_ref().unwrap();
1840                shell.capture_event();
1841
1842                shell.publish(surface_action(crate::surface::action::destroy_popup(_id)));
1843            }
1844            state.show_context = None;
1845
1846            state.menu_state.inner.with_data_mut(|data| {
1847                // Clear stale MenuBounds from any previous context menu before opening a new one.
1848                data.reset();
1849                data.open = false;
1850                data.view_cursor = cursor_position;
1851            });
1852        }
1853
1854        if matches!(
1855            event,
1856            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
1857        ) {
1858            state.tab_drag_candidate = None;
1859        }
1860
1861        if state.is_focused() {
1862            if let Event::Keyboard(keyboard::Event::KeyPressed {
1863                key: keyboard::Key::Named(keyboard::key::Named::Tab),
1864                modifiers,
1865                ..
1866            }) = event
1867            {
1868                state.focused_visible = true;
1869                return if *modifiers == keyboard::Modifiers::SHIFT {
1870                    self.focus_previous(state, shell);
1871                } else if modifiers.is_empty() {
1872                    self.focus_next(state, shell);
1873                };
1874            }
1875
1876            if let Some(on_activate) = self.on_activate.as_ref()
1877                && let Event::Keyboard(keyboard::Event::KeyReleased {
1878                    key: keyboard::Key::Named(keyboard::key::Named::Enter),
1879                    ..
1880                }) = event
1881            {
1882                match state.focused_item {
1883                    Item::Tab(entity) => {
1884                        shell.publish(on_activate(entity));
1885                    }
1886
1887                    Item::PrevButton => {
1888                        if self.prev_tab_sensitive(state) {
1889                            state.buttons_offset -= 1;
1890
1891                            // If the change would cause it to be insensitive, focus the first tab.
1892                            if !self.prev_tab_sensitive(state)
1893                                && let Some(first) = self.first_tab(state)
1894                            {
1895                                state.focused_item = Item::Tab(first);
1896                            }
1897                        }
1898                    }
1899
1900                    Item::NextButton => {
1901                        if self.next_tab_sensitive(state) {
1902                            state.buttons_offset += 1;
1903
1904                            // If the change would cause it to be insensitive, focus the last tab.
1905                            if !self.next_tab_sensitive(state)
1906                                && let Some(last) = self.last_tab(state)
1907                            {
1908                                state.focused_item = Item::Tab(last);
1909                            }
1910                        }
1911                    }
1912
1913                    Item::None | Item::Set => (),
1914                }
1915
1916                shell.capture_event();
1917            }
1918        }
1919    }
1920
1921    fn operate(
1922        &mut self,
1923        tree: &mut Tree,
1924        layout: Layout<'_>,
1925        _renderer: &Renderer,
1926        operation: &mut dyn iced_core::widget::Operation<()>,
1927    ) {
1928        let state = tree.state.downcast_mut::<LocalState>();
1929        operation.focusable(Some(&self.id.0), layout.bounds(), state);
1930        operation.custom(Some(&self.id.0), layout.bounds(), state);
1931
1932        if let Item::Set = state.focused_item {
1933            if self.prev_tab_sensitive(state) {
1934                state.focused_item = Item::PrevButton;
1935            } else if let Some(first) = self.first_tab(state) {
1936                state.focused_item = Item::Tab(first);
1937            }
1938        }
1939    }
1940
1941    fn mouse_interaction(
1942        &self,
1943        tree: &Tree,
1944        layout: Layout<'_>,
1945        cursor_position: mouse::Cursor,
1946        _viewport: &iced::Rectangle,
1947        _renderer: &Renderer,
1948    ) -> iced_core::mouse::Interaction {
1949        if self.on_activate.is_none() {
1950            return iced_core::mouse::Interaction::default();
1951        }
1952        let state = tree.state.downcast_ref::<LocalState>();
1953        let bounds = layout.bounds();
1954
1955        if cursor_position.is_over(bounds) {
1956            let hovered_button = self
1957                .variant_bounds(state, bounds)
1958                .filter_map(|item| match item {
1959                    ItemBounds::Button(entity, bounds) => Some((entity, bounds)),
1960                    _ => None,
1961                })
1962                .find(|(_key, bounds)| cursor_position.is_over(*bounds));
1963
1964            if let Some((key, _bounds)) = hovered_button {
1965                return if self.model.items[key].enabled {
1966                    iced_core::mouse::Interaction::Pointer
1967                } else {
1968                    iced_core::mouse::Interaction::Idle
1969                };
1970            }
1971        }
1972
1973        iced_core::mouse::Interaction::default()
1974    }
1975
1976    #[allow(clippy::too_many_lines)]
1977    fn draw(
1978        &self,
1979        tree: &Tree,
1980        renderer: &mut Renderer,
1981        theme: &crate::Theme,
1982        style: &renderer::Style,
1983        layout: Layout<'_>,
1984        cursor: mouse::Cursor,
1985        viewport: &iced::Rectangle,
1986    ) {
1987        let state = tree.state.downcast_ref::<LocalState>();
1988        let appearance = Self::variant_appearance(theme, &self.style);
1989        let bounds: Rectangle = layout.bounds();
1990        let button_amount = self.model.items.len();
1991        let show_drop_hint = state.dragging_tab.is_some();
1992        let drop_hint = if show_drop_hint {
1993            state.drop_hint
1994        } else {
1995            None
1996        };
1997
1998        // Draw the background, if a background was defined.
1999        if let Some(background) = appearance.background {
2000            renderer.fill_quad(
2001                renderer::Quad {
2002                    bounds,
2003                    border: appearance.border,
2004                    shadow: Shadow::default(),
2005                    snap: true,
2006                },
2007                background,
2008            );
2009        }
2010
2011        // Draw previous and next tab buttons if there is a need to paginate tabs.
2012        if state.collapsed {
2013            let mut tab_bounds = prev_tab_bounds(&bounds, f32::from(self.button_height));
2014
2015            // Previous tab button
2016            let mut background_appearance =
2017                if self.on_activate.is_some() && Item::PrevButton == state.focused_item {
2018                    Some(appearance.active)
2019                } else if self.on_activate.is_some() && Item::PrevButton == state.hovered {
2020                    Some(appearance.hover)
2021                } else {
2022                    None
2023                };
2024
2025            if let Some(background_appearance) = background_appearance.take() {
2026                renderer.fill_quad(
2027                    renderer::Quad {
2028                        bounds: tab_bounds,
2029                        border: Border {
2030                            radius: theme.cosmic().radius_s().into(),
2031                            ..Default::default()
2032                        },
2033                        shadow: Shadow::default(),
2034                        snap: true,
2035                    },
2036                    background_appearance
2037                        .background
2038                        .unwrap_or(Background::Color(Color::TRANSPARENT)),
2039                );
2040            }
2041
2042            draw_icon::<Message>(
2043                renderer,
2044                theme,
2045                style,
2046                cursor,
2047                viewport,
2048                if state.buttons_offset == 0 {
2049                    appearance.inactive.text_color
2050                } else {
2051                    appearance.active.text_color
2052                },
2053                Rectangle {
2054                    x: tab_bounds.x + 8.0,
2055                    y: tab_bounds.y + f32::from(self.button_height) / 4.0,
2056                    width: 16.0,
2057                    height: 16.0,
2058                },
2059                icon::from_name("go-previous-symbolic").size(16).icon(),
2060            );
2061
2062            tab_bounds = next_tab_bounds(&bounds, f32::from(self.button_height));
2063
2064            // Next tab button
2065            background_appearance =
2066                if self.on_activate.is_some() && Item::NextButton == state.focused_item {
2067                    Some(appearance.active)
2068                } else if self.on_activate.is_some() && Item::NextButton == state.hovered {
2069                    Some(appearance.hover)
2070                } else {
2071                    None
2072                };
2073
2074            if let Some(background_appearance) = background_appearance {
2075                renderer.fill_quad(
2076                    renderer::Quad {
2077                        bounds: tab_bounds,
2078                        border: Border {
2079                            radius: theme.cosmic().radius_s().into(),
2080                            ..Default::default()
2081                        },
2082                        shadow: Shadow::default(),
2083                        snap: true,
2084                    },
2085                    background_appearance
2086                        .background
2087                        .unwrap_or(Background::Color(Color::TRANSPARENT)),
2088                );
2089            }
2090
2091            draw_icon::<Message>(
2092                renderer,
2093                theme,
2094                style,
2095                cursor,
2096                viewport,
2097                if self.next_tab_sensitive(state) {
2098                    appearance.active.text_color
2099                } else if let Item::NextButton = state.focused_item {
2100                    appearance.active.text_color
2101                } else {
2102                    appearance.inactive.text_color
2103                },
2104                Rectangle {
2105                    x: tab_bounds.x + 8.0,
2106                    y: tab_bounds.y + f32::from(self.button_height) / 4.0,
2107                    width: 16.0,
2108                    height: 16.0,
2109                },
2110                icon::from_name("go-next-symbolic").size(16).icon(),
2111            );
2112        }
2113
2114        let rad_0 = THEME.lock().unwrap().cosmic().corner_radii.radius_0;
2115
2116        let divider_background = Background::Color(
2117            crate::theme::active()
2118                .cosmic()
2119                .primary_component_divider()
2120                .into(),
2121        );
2122
2123        // Draw each of the items in the widget.
2124        let mut nth = 0;
2125        let drop_hint_marker = drop_hint;
2126        let show_drop_hint_marker = show_drop_hint;
2127        self.variant_bounds(state, bounds).for_each(move |item| {
2128            let (key, mut bounds) = match item {
2129                // Draw a button
2130                ItemBounds::Button(entity, bounds) => (entity, bounds),
2131
2132                // Draw a divider between buttons
2133                ItemBounds::Divider(bounds, accented) => {
2134                    renderer.fill_quad(
2135                        renderer::Quad {
2136                            bounds,
2137                            border: Border::default(),
2138                            shadow: Shadow::default(),
2139                            snap: true,
2140                        },
2141                        {
2142                            let theme = crate::theme::active();
2143                            if accented {
2144                                Background::Color(theme.cosmic().small_widget_divider().into())
2145                            } else {
2146                                Background::Color(theme.cosmic().primary_container_divider().into())
2147                            }
2148                        },
2149                    );
2150
2151                    return;
2152                }
2153            };
2154
2155            let original_bounds = bounds;
2156            let center_y = bounds.center_y();
2157
2158            if show_drop_hint_marker
2159                && matches!(
2160                    drop_hint_marker,
2161                    Some(DropHint {
2162                        entity,
2163                        side: DropSide::Before
2164                    }) if entity == key
2165                )
2166            {
2167                draw_drop_indicator(
2168                    renderer,
2169                    original_bounds,
2170                    DropSide::Before,
2171                    Self::VERTICAL,
2172                    appearance.active.text_color,
2173                );
2174            }
2175
2176            let menu_open = || {
2177                state.show_context == Some(key)
2178                    && !tree.children.is_empty()
2179                    && tree.children[0]
2180                        .state
2181                        .downcast_ref::<MenuBarState>()
2182                        .inner
2183                        .with_data(|data| data.open)
2184            };
2185
2186            let key_is_active = self.model.is_active(key);
2187            let key_is_focused = state.focused_visible && self.button_is_focused(state, key);
2188            let key_is_hovered = self.button_is_hovered(state, key);
2189            let status_appearance = if self.button_is_pressed(state, key) {
2190                appearance.pressed
2191            } else if key_is_hovered || menu_open() {
2192                appearance.hover
2193            } else if key_is_active {
2194                appearance.active
2195            } else {
2196                appearance.inactive
2197            };
2198
2199            let button_appearance = if nth == 0 {
2200                status_appearance.first
2201            } else if nth + 1 == button_amount {
2202                status_appearance.last
2203            } else {
2204                status_appearance.middle
2205            };
2206
2207            // Draw the active hint on tabs
2208            if appearance.active_width > 0.0 {
2209                let active_width = if key_is_active {
2210                    appearance.active_width
2211                } else {
2212                    1.0
2213                };
2214
2215                renderer.fill_quad(
2216                    renderer::Quad {
2217                        bounds: if Self::VERTICAL {
2218                            Rectangle {
2219                                x: bounds.x + bounds.width - active_width,
2220                                width: active_width,
2221                                ..bounds
2222                            }
2223                        } else {
2224                            Rectangle {
2225                                y: bounds.y + bounds.height - active_width,
2226                                height: active_width,
2227                                ..bounds
2228                            }
2229                        },
2230                        border: Border {
2231                            radius: rad_0.into(),
2232                            ..Default::default()
2233                        },
2234                        shadow: Shadow::default(),
2235                        snap: true,
2236                    },
2237                    appearance.active.text_color,
2238                );
2239            }
2240
2241            bounds.x += f32::from(self.button_padding[0]);
2242            bounds.width -= f32::from(self.button_padding[0]) - f32::from(self.button_padding[2]);
2243            let mut indent_padding = 0.0;
2244
2245            // Adjust bounds by indent
2246            if let Some(indent) = self.model.indent(key)
2247                && indent > 0
2248            {
2249                let adjustment = f32::from(indent) * f32::from(self.indent_spacing);
2250                bounds.x += adjustment;
2251                bounds.width -= adjustment;
2252
2253                // Draw indent line
2254                if let crate::theme::SegmentedButton::FileNav = self.style
2255                    && indent > 1
2256                {
2257                    indent_padding = 7.0;
2258
2259                    for level in 1..indent {
2260                        renderer.fill_quad(
2261                            renderer::Quad {
2262                                bounds: Rectangle {
2263                                    x: (level as f32)
2264                                        .mul_add(-(self.indent_spacing as f32), bounds.x)
2265                                        + indent_padding,
2266                                    width: 1.0,
2267                                    ..bounds
2268                                },
2269                                border: Border {
2270                                    radius: rad_0.into(),
2271                                    ..Default::default()
2272                                },
2273                                shadow: Shadow::default(),
2274                                snap: true,
2275                            },
2276                            divider_background,
2277                        );
2278                    }
2279
2280                    indent_padding += 4.0;
2281                }
2282            }
2283
2284            // Render the background of the button.
2285            if key_is_focused || status_appearance.background.is_some() {
2286                renderer.fill_quad(
2287                    renderer::Quad {
2288                        bounds: Rectangle {
2289                            x: bounds.x - f32::from(self.button_padding[0]) + indent_padding,
2290                            width: bounds.width + f32::from(self.button_padding[0])
2291                                - f32::from(self.button_padding[2])
2292                                - indent_padding,
2293                            ..bounds
2294                        },
2295                        border: if key_is_focused {
2296                            Border {
2297                                width: 1.0,
2298                                color: appearance.active.text_color,
2299                                radius: button_appearance.border.radius,
2300                            }
2301                        } else {
2302                            button_appearance.border
2303                        },
2304                        shadow: Shadow::default(),
2305                        snap: true,
2306                    },
2307                    status_appearance
2308                        .background
2309                        .unwrap_or(Background::Color(Color::TRANSPARENT)),
2310                );
2311            }
2312
2313            // Align contents of the button to the requested `button_alignment`.
2314            {
2315                // Avoid shifting content outside the left edge when the measured content is
2316                // wider than the available button bounds (for example, non-ellipsized text).
2317                let actual_width = state.internal_layout[nth].1.width.min(bounds.width);
2318
2319                let offset = match self.button_alignment {
2320                    Alignment::Start => None,
2321                    Alignment::Center => Some((bounds.width - actual_width) / 2.0),
2322                    Alignment::End => Some(bounds.width - actual_width),
2323                };
2324
2325                if let Some(offset) = offset {
2326                    bounds.x += offset - f32::from(self.button_padding[0]);
2327                    bounds.width = actual_width;
2328                }
2329            }
2330
2331            // Draw the image beside the text.
2332            if let Some(icon) = self.model.icon(key) {
2333                let mut image_bounds = bounds;
2334                let width = f32::from(icon.size);
2335                let offset = width + f32::from(self.button_spacing);
2336                image_bounds.y = center_y - width / 2.0;
2337
2338                draw_icon::<Message>(
2339                    renderer,
2340                    theme,
2341                    style,
2342                    cursor,
2343                    viewport,
2344                    status_appearance.text_color,
2345                    Rectangle {
2346                        width,
2347                        height: width,
2348                        ..image_bounds
2349                    },
2350                    icon.clone(),
2351                );
2352
2353                bounds.x += offset;
2354            } else {
2355                // Draw the selection indicator if widget is a segmented selection, and the item is selected.
2356                if key_is_active && let crate::theme::SegmentedButton::Control = self.style {
2357                    let mut image_bounds = bounds;
2358                    image_bounds.y = center_y - 8.0;
2359
2360                    draw_icon::<Message>(
2361                        renderer,
2362                        theme,
2363                        style,
2364                        cursor,
2365                        viewport,
2366                        status_appearance.text_color,
2367                        Rectangle {
2368                            width: 16.0,
2369                            height: 16.0,
2370                            ..image_bounds
2371                        },
2372                        crate::widget::icon(match crate::widget::common::object_select().data() {
2373                            iced_core::svg::Data::Bytes(bytes) => {
2374                                crate::widget::icon::from_svg_bytes(bytes.as_ref()).symbolic(true)
2375                            }
2376                            iced_core::svg::Data::Path(path) => {
2377                                crate::widget::icon::from_path(path.clone())
2378                            }
2379                        }),
2380                    );
2381
2382                    let offset = 16.0 + f32::from(self.button_spacing);
2383
2384                    bounds.x += offset;
2385                }
2386            }
2387
2388            // Whether to show the close button on this tab.
2389            let show_close_button =
2390                (key_is_active || !self.show_close_icon_on_hover || key_is_hovered)
2391                    && self.model.is_closable(key);
2392
2393            // Width of the icon used by the close button, which we will subtract from the text bounds.
2394            let close_icon_width = if show_close_button {
2395                f32::from(self.close_icon.size)
2396            } else {
2397                0.0
2398            };
2399
2400            bounds.width = original_bounds.width
2401                - (bounds.x - original_bounds.x)
2402                - close_icon_width
2403                - f32::from(self.button_padding[2]);
2404
2405            bounds.y = center_y;
2406
2407            if self.model.text(key).is_some_and(|text| !text.is_empty()) {
2408                // FIXME why has this behavior changed? Does the center alignment not work with infinite bounds now?
2409                bounds.y -= state.paragraphs[key].min_height() / 2.;
2410
2411                // Draw the text for this segmented button or tab.
2412                renderer.fill_paragraph(
2413                    state.paragraphs[key].raw(),
2414                    bounds.position(),
2415                    status_appearance.text_color,
2416                    Rectangle {
2417                        x: bounds.x,
2418                        width: bounds.width,
2419                        height: original_bounds.height,
2420                        y: bounds.y,
2421                        //  ..original_bounds,
2422                    },
2423                );
2424            }
2425
2426            // Draw a close button if set.
2427            if show_close_button {
2428                let close_button_bounds = close_bounds(original_bounds, close_icon_width);
2429
2430                draw_icon::<Message>(
2431                    renderer,
2432                    theme,
2433                    style,
2434                    cursor,
2435                    viewport,
2436                    status_appearance.text_color,
2437                    close_button_bounds,
2438                    self.close_icon.clone(),
2439                );
2440            }
2441
2442            if show_drop_hint_marker {
2443                if matches!(
2444                    drop_hint_marker,
2445                    Some(DropHint {
2446                        entity,
2447                        side: DropSide::After
2448                    }) if entity == key
2449                ) {
2450                    draw_drop_indicator(
2451                        renderer,
2452                        original_bounds,
2453                        DropSide::After,
2454                        Self::VERTICAL,
2455                        appearance.active.text_color,
2456                    );
2457                }
2458            }
2459
2460            nth += 1;
2461        });
2462    }
2463
2464    fn overlay<'b>(
2465        &'b mut self,
2466        tree: &'b mut Tree,
2467        layout: iced_core::Layout<'b>,
2468        _renderer: &Renderer,
2469        _viewport: &iced_core::Rectangle,
2470        translation: Vector,
2471    ) -> Option<iced_core::overlay::Element<'b, Message, crate::Theme, Renderer>> {
2472        #[cfg(wayland_platform)]
2473        if matches!(
2474            crate::app::cosmic::WINDOWING_SYSTEM.get(),
2475            Some(crate::app::cosmic::WindowingSystem::Wayland)
2476        ) && self.on_surface_action.is_some()
2477            && self.window_id != window::Id::NONE
2478        {
2479            return None;
2480        }
2481
2482        let state = tree.state.downcast_mut::<LocalState>();
2483        let menu_state = state.menu_state.clone();
2484
2485        let entity = state.show_context?;
2486
2487        let (mut bounds, i) = self
2488            .variant_bounds(state, layout.bounds())
2489            .filter_map(|item| match item {
2490                ItemBounds::Button(entity, bounds) => Some((bounds, entity)),
2491                _ => None,
2492            })
2493            .enumerate()
2494            .find_map(|(i, (bounds, e))| if e == entity { Some((bounds, i)) } else { None })?;
2495
2496        assert!(
2497            self.context_menu
2498                .as_ref()
2499                .is_none_or(|m| m[0].children.len() == self.model.len())
2500        );
2501        let menu = self
2502            .context_menu
2503            .as_mut()
2504            .map(|m| m[0].children[i].clone())?;
2505
2506        if !menu_state.inner.with_data(|data| data.open) {
2507            // If the menu is not open, we don't need to show it.
2508            // We also clear the context entity and update the text
2509            // cache so that the item is not bold when the context menu is closed
2510            state.show_context = None;
2511            for key in self.model.order.iter().copied() {
2512                self.update_entity_paragraph(state, key);
2513            }
2514            return None;
2515        }
2516        bounds.x = state.context_cursor.x;
2517        bounds.y = state.context_cursor.y;
2518
2519        Some(
2520            crate::widget::menu::Menu {
2521                tree: menu_state,
2522                menu_roots: std::borrow::Cow::Owned(vec![menu]),
2523                bounds_expand: 16,
2524                menu_overlays_parent: true,
2525                close_condition: CloseCondition {
2526                    leave: false,
2527                    click_outside: true,
2528                    click_inside: true,
2529                },
2530                item_width: ItemWidth::Uniform(240),
2531                item_height: ItemHeight::Dynamic(40),
2532                bar_bounds: bounds,
2533                main_offset: -bounds.height as i32,
2534                cross_offset: 0,
2535                root_bounds_list: vec![bounds],
2536                path_highlight: Some(PathHighlight::MenuActive),
2537                style: std::borrow::Cow::Borrowed(&crate::theme::menu_bar::MenuBarStyle::Default),
2538                position: Point::new(translation.x, translation.y),
2539                is_overlay: true,
2540                window_id: window::Id::NONE,
2541                depth: 0,
2542                on_surface_action: None,
2543            }
2544            .overlay(),
2545        )
2546    }
2547
2548    fn drag_destinations(
2549        &self,
2550        tree: &Tree,
2551        layout: Layout<'_>,
2552        _renderer: &Renderer,
2553        dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
2554    ) {
2555        let local_state = tree.state.downcast_ref::<LocalState>();
2556        let my_id = self.get_drag_id();
2557        let mut pushed = false;
2558
2559        for item in self.variant_bounds(local_state, layout.bounds()) {
2560            if let ItemBounds::Button(_entity, rect) = item {
2561                pushed = true;
2562                log::trace!(
2563                    target: TAB_REORDER_LOG_TARGET,
2564                    "register drag destination id={:?} bounds=({:.2},{:.2},{:.2},{:.2}) mimes={:?}",
2565                    my_id,
2566                    rect.x,
2567                    rect.y,
2568                    rect.width,
2569                    rect.height,
2570                    self.mimes
2571                );
2572                dnd_rectangles.push(DndDestinationRectangle {
2573                    id: my_id,
2574                    rectangle: dnd::Rectangle {
2575                        x: f64::from(rect.x),
2576                        y: f64::from(rect.y),
2577                        width: f64::from(rect.width),
2578                        height: f64::from(rect.height),
2579                    },
2580                    mime_types: self.mimes.clone().into_iter().map(Cow::Owned).collect(),
2581                    actions: DndAction::Copy | DndAction::Move,
2582                    preferred: DndAction::Move,
2583                });
2584            }
2585        }
2586
2587        if let Some(mime) = self.tab_drag.as_ref().map(|d| &d.mime) {
2588            for item in self.variant_bounds(local_state, layout.bounds()) {
2589                if let ItemBounds::Button(_entity, rect) = item {
2590                    pushed = true;
2591                    log::trace!(
2592                        target: TAB_REORDER_LOG_TARGET,
2593                        "register drag destination id={:?} bounds=({:.2},{:.2},{:.2},{:.2}) mimes={:?}",
2594                        my_id,
2595                        rect.x,
2596                        rect.y,
2597                        rect.width,
2598                        rect.height,
2599                        mime
2600                    );
2601                    dnd_rectangles.push(DndDestinationRectangle {
2602                        id: my_id,
2603                        rectangle: dnd::Rectangle {
2604                            x: f64::from(rect.x),
2605                            y: f64::from(rect.y),
2606                            width: f64::from(rect.width),
2607                            height: f64::from(rect.height),
2608                        },
2609                        mime_types: vec![Cow::Owned(mime.clone())],
2610                        actions: DndAction::Copy | DndAction::Move,
2611                        preferred: DndAction::Move,
2612                    });
2613                }
2614            }
2615        }
2616
2617        if !pushed {
2618            let bounds = layout.bounds();
2619            log::trace!(
2620                target: TAB_REORDER_LOG_TARGET,
2621                "register drag destination id={:?} bounds=({:.2},{:.2},{:.2},{:.2}) mimes={:?}",
2622                my_id,
2623                bounds.x,
2624                bounds.y,
2625                bounds.width,
2626                bounds.height,
2627                self.mimes
2628            );
2629            dnd_rectangles.push(DndDestinationRectangle {
2630                id: my_id,
2631                rectangle: dnd::Rectangle {
2632                    x: f64::from(bounds.x),
2633                    y: f64::from(bounds.y),
2634                    width: f64::from(bounds.width),
2635                    height: f64::from(bounds.height),
2636                },
2637                mime_types: self.mimes.clone().into_iter().map(Cow::Owned).collect(),
2638                actions: DndAction::Copy | DndAction::Move,
2639                preferred: DndAction::Move,
2640            });
2641        }
2642    }
2643}
2644
2645impl<'a, Variant, SelectionMode, Message> From<SegmentedButton<'a, Variant, SelectionMode, Message>>
2646    for Element<'a, Message>
2647where
2648    SegmentedButton<'a, Variant, SelectionMode, Message>: SegmentedVariant,
2649    Variant: 'static,
2650    Model<SelectionMode>: Selectable,
2651    SelectionMode: Default,
2652    Message: 'static + Clone,
2653{
2654    fn from(mut widget: SegmentedButton<'a, Variant, SelectionMode, Message>) -> Self {
2655        if widget.model.items.is_empty() {
2656            widget.spacing = 0;
2657        }
2658
2659        Self::new(widget)
2660    }
2661}
2662
2663struct TabDragSource<Message> {
2664    mime: String,
2665    threshold: f32,
2666    _marker: PhantomData<Message>,
2667}
2668
2669impl<Message> TabDragSource<Message> {
2670    fn new(mime: String) -> Self {
2671        Self {
2672            mime,
2673            threshold: 8.0,
2674            _marker: PhantomData,
2675        }
2676    }
2677}
2678
2679struct SimpleDragData {
2680    mime: String,
2681    bytes: Vec<u8>,
2682}
2683
2684impl SimpleDragData {
2685    fn new(mime: String, bytes: Vec<u8>) -> Self {
2686        Self { mime, bytes }
2687    }
2688}
2689
2690impl iced::clipboard::mime::AsMimeTypes for SimpleDragData {
2691    fn available(&self) -> Cow<'static, [String]> {
2692        Cow::Owned(vec![self.mime.clone()])
2693    }
2694
2695    fn as_bytes(&self, mime_type: &str) -> Option<Cow<'static, [u8]>> {
2696        if mime_type == self.mime {
2697            Some(Cow::Owned(self.bytes.clone()))
2698        } else {
2699            None
2700        }
2701    }
2702}
2703
2704#[derive(Clone, Copy)]
2705struct TabDragCandidate {
2706    entity: Entity,
2707    bounds: Rectangle,
2708    origin: Point,
2709}
2710
2711#[derive(Debug, Clone, Copy)]
2712struct Focus {
2713    updated_at: Instant,
2714    now: Instant,
2715}
2716
2717/// State that is maintained by each individual widget.
2718pub struct LocalState {
2719    /// Menu state
2720    pub(crate) menu_state: MenuBarState,
2721    /// Defines how many buttons to show at a time.
2722    pub(super) buttons_visible: usize,
2723    /// Button visibility offset, when collapsed.
2724    pub(super) buttons_offset: usize,
2725    /// Whether buttons need to be collapsed to preserve minimum width
2726    pub(super) collapsed: bool,
2727    /// Visibility of focus state
2728    focused_visible: bool,
2729    /// If the widget is focused or not.
2730    focused: Option<Focus>,
2731    /// The key inside the widget that is currently focused.
2732    focused_item: Item,
2733    /// The ID of the button that is being hovered. Defaults to null.
2734    hovered: Item,
2735    /// The ID of the button that was middle-clicked, but not yet released.
2736    middle_clicked: Option<Item>,
2737    /// Last known length of the model.
2738    pub(super) known_length: usize,
2739    /// Dimensions of internal buttons when shrinking
2740    pub(super) internal_layout: Vec<(Size, Size)>,
2741    /// The paragraphs for each text.
2742    paragraphs: SecondaryMap<Entity, crate::Plain>,
2743    /// Used to detect changes in text.
2744    text_hashes: SecondaryMap<Entity, u64>,
2745    /// Location of cursor when context menu was opened.
2746    context_cursor: Point,
2747    /// Track whether an item is currently showing a context menu.
2748    show_context: Option<Entity>,
2749    /// Time since last tab activation from wheel movements.
2750    wheel_timestamp: Option<Instant>,
2751    /// Dnd state
2752    pub dnd_state: crate::widget::dnd_destination::State<Option<Entity>>,
2753    /// Dnd state
2754    pub offer_mimes: Vec<String>,
2755    /// Tracks multi-touch events
2756    fingers_pressed: HashSet<Finger>,
2757    /// The currently pressed item
2758    pressed_item: Option<Item>,
2759    /// Pending tab drag candidate data
2760    tab_drag_candidate: Option<TabDragCandidate>,
2761    /// Currently dragging tab entity
2762    dragging_tab: Option<Entity>,
2763    /// Current drop hint for drag-and-drop indicator
2764    drop_hint: Option<DropHint>,
2765}
2766
2767#[derive(Debug, Default, PartialEq)]
2768enum Item {
2769    NextButton,
2770    #[default]
2771    None,
2772    PrevButton,
2773    Set,
2774    Tab(Entity),
2775}
2776
2777impl LocalState {
2778    fn set_focused(&mut self) {
2779        let now = Instant::now();
2780        LAST_FOCUS_UPDATE.with(|x| x.set(now));
2781
2782        self.focused = Some(Focus {
2783            updated_at: now,
2784            now,
2785        });
2786    }
2787}
2788
2789#[cfg(test)]
2790mod tests {
2791    use super::*;
2792    use crate::widget::segmented_button::{self, Appearance as SegAppearance};
2793    use iced::Size;
2794    use slotmap::SecondaryMap;
2795    use std::collections::HashSet;
2796
2797    #[derive(Clone, Debug)]
2798    enum TestMessage {}
2799
2800    struct TestVariant;
2801
2802    impl<SelectionMode, Message> SegmentedVariant
2803        for SegmentedButton<'_, TestVariant, SelectionMode, Message>
2804    where
2805        Model<SelectionMode>: Selectable,
2806        SelectionMode: Default,
2807        Message: Clone,
2808    {
2809        const VERTICAL: bool = false;
2810
2811        fn variant_appearance(
2812            _theme: &crate::Theme,
2813            _style: &crate::theme::SegmentedButton,
2814        ) -> SegAppearance {
2815            SegAppearance::default()
2816        }
2817
2818        fn variant_bounds<'b>(
2819            &'b self,
2820            _state: &'b LocalState,
2821            bounds: Rectangle,
2822        ) -> Box<dyn Iterator<Item = ItemBounds> + 'b> {
2823            let len = self.model.order.len();
2824            if len == 0 {
2825                return Box::new(std::iter::empty());
2826            }
2827            let width = bounds.width / len as f32;
2828            Box::new(
2829                self.model
2830                    .order
2831                    .iter()
2832                    .copied()
2833                    .enumerate()
2834                    .map(move |(idx, entity)| {
2835                        let rect = Rectangle {
2836                            x: bounds.x + (idx as f32) * width,
2837                            y: bounds.y,
2838                            width,
2839                            height: bounds.height,
2840                        };
2841                        ItemBounds::Button(entity, rect)
2842                    }),
2843            )
2844        }
2845
2846        fn variant_layout(
2847            &self,
2848            _state: &mut LocalState,
2849            _renderer: &crate::Renderer,
2850            _limits: &layout::Limits,
2851        ) -> Size {
2852            Size::ZERO
2853        }
2854    }
2855
2856    fn sample_model() -> (
2857        segmented_button::SingleSelectModel,
2858        Vec<segmented_button::Entity>,
2859    ) {
2860        let mut entities = Vec::new();
2861        let model = segmented_button::Model::builder()
2862            .insert(|b| b.text("One").with_id(|id| entities.push(id)))
2863            .insert(|b| b.text("Two").with_id(|id| entities.push(id)))
2864            .insert(|b| b.text("Three").with_id(|id| entities.push(id)))
2865            .build();
2866        (model, entities)
2867    }
2868
2869    fn test_state(dragging: segmented_button::Entity, len: usize) -> LocalState {
2870        let mut state = LocalState {
2871            menu_state: MenuBarState::default(),
2872            paragraphs: SecondaryMap::new(),
2873            text_hashes: SecondaryMap::new(),
2874            buttons_visible: 0,
2875            buttons_offset: 0,
2876            collapsed: false,
2877            focused: None,
2878            focused_item: Item::default(),
2879            focused_visible: false,
2880            hovered: Item::default(),
2881            known_length: 0,
2882            middle_clicked: None,
2883            internal_layout: Vec::new(),
2884            context_cursor: Point::ORIGIN,
2885            show_context: None,
2886            wheel_timestamp: None,
2887            dnd_state: crate::widget::dnd_destination::State::<Option<Entity>>::new(),
2888            fingers_pressed: HashSet::new(),
2889            pressed_item: None,
2890            tab_drag_candidate: None,
2891            dragging_tab: Some(dragging),
2892            drop_hint: None,
2893            offer_mimes: Vec::new(),
2894        };
2895        state.buttons_visible = len;
2896        state.known_length = len;
2897        state
2898    }
2899
2900    #[test]
2901    fn drop_hint_reports_before_and_after() {
2902        let (model, ids) = sample_model();
2903        let button =
2904            SegmentedButton::<TestVariant, segmented_button::SingleSelect, TestMessage>::new(
2905                &model,
2906            );
2907        let state = test_state(ids[0], model.order.len());
2908        let bounds = Rectangle {
2909            x: 0.0,
2910            y: 0.0,
2911            width: 300.0,
2912            height: 30.0,
2913        };
2914        let before = button
2915            .drop_hint_for_position(&state, bounds, Point::new(10.0, 15.0))
2916            .expect("hint");
2917        assert_eq!(before.entity, ids[0]);
2918        assert!(matches!(before.side, DropSide::Before));
2919
2920        let after = button
2921            .drop_hint_for_position(&state, bounds, Point::new(290.0, 15.0))
2922            .expect("hint");
2923        assert_eq!(after.entity, ids[2]);
2924        assert!(matches!(after.side, DropSide::After));
2925    }
2926}
2927
2928impl operation::Focusable for LocalState {
2929    fn is_focused(&self) -> bool {
2930        self.focused
2931            .is_some_and(|f| f.updated_at == LAST_FOCUS_UPDATE.with(|f| f.get()))
2932    }
2933
2934    fn focus(&mut self) {
2935        self.set_focused();
2936        self.focused_visible = true;
2937        self.focused_item = Item::Set;
2938    }
2939
2940    fn unfocus(&mut self) {
2941        self.focused = None;
2942        self.focused_item = Item::None;
2943        self.focused_visible = false;
2944        self.show_context = None;
2945    }
2946}
2947
2948/// The iced identifier of a segmented button.
2949#[derive(Debug, Clone, PartialEq)]
2950pub struct Id(widget::Id);
2951
2952impl Id {
2953    /// Creates a custom [`Id`].
2954    pub fn new(id: impl Into<std::borrow::Cow<'static, str>>) -> Self {
2955        Self(widget::Id::new(id))
2956    }
2957
2958    /// Creates a unique [`Id`].
2959    ///
2960    /// This function produces a different [`Id`] every time it is called.
2961    #[must_use]
2962    #[inline]
2963    pub fn unique() -> Self {
2964        Self(widget::Id::unique())
2965    }
2966}
2967
2968impl From<Id> for widget::Id {
2969    fn from(id: Id) -> Self {
2970        id.0
2971    }
2972}
2973
2974/// Calculates the bounds of the close button within the area of an item.
2975fn close_bounds(area: Rectangle<f32>, icon_size: f32) -> Rectangle<f32> {
2976    Rectangle {
2977        x: area.x + area.width - icon_size - 8.0,
2978        y: area.center_y() - (icon_size / 2.0),
2979        width: icon_size,
2980        height: icon_size,
2981    }
2982}
2983
2984/// Calculate the bounds of the `next_tab` button.
2985fn next_tab_bounds(bounds: &Rectangle, button_height: f32) -> Rectangle {
2986    Rectangle {
2987        x: bounds.x + bounds.width - button_height,
2988        y: bounds.y,
2989        width: button_height,
2990        height: button_height,
2991    }
2992}
2993
2994/// Calculate the bounds of the `prev_tab` button.
2995fn prev_tab_bounds(bounds: &Rectangle, button_height: f32) -> Rectangle {
2996    Rectangle {
2997        x: bounds.x,
2998        y: bounds.y,
2999        width: button_height,
3000        height: button_height,
3001    }
3002}
3003
3004#[allow(clippy::too_many_arguments)]
3005fn draw_icon<Message: 'static>(
3006    renderer: &mut Renderer,
3007    theme: &crate::Theme,
3008    style: &renderer::Style,
3009    cursor: mouse::Cursor,
3010    viewport: &Rectangle,
3011    color: Color,
3012    bounds: Rectangle,
3013    icon: Icon,
3014) {
3015    let layout_node = layout::Node::new(Size {
3016        width: bounds.width,
3017        height: bounds.width,
3018    })
3019    .move_to(Point {
3020        x: bounds.x,
3021        y: bounds.y,
3022    });
3023
3024    Widget::<Message, crate::Theme, Renderer>::draw(
3025        Element::<Message>::from(icon).as_widget(),
3026        &Tree::empty(),
3027        renderer,
3028        theme,
3029        &renderer::Style {
3030            icon_color: color,
3031            text_color: color,
3032            scale_factor: style.scale_factor,
3033        },
3034        Layout::new(&layout_node),
3035        cursor,
3036        viewport,
3037    );
3038}
3039
3040fn draw_drop_indicator(
3041    renderer: &mut Renderer,
3042    bounds: Rectangle,
3043    side: DropSide,
3044    vertical: bool,
3045    color: Color,
3046) {
3047    let thickness = 4.0;
3048    let quad_bounds = if vertical {
3049        let y = match side {
3050            DropSide::Before => bounds.y - thickness / 2.0,
3051            DropSide::After => bounds.y + bounds.height - thickness / 2.0,
3052        };
3053
3054        Rectangle {
3055            x: bounds.x,
3056            y,
3057            width: bounds.width,
3058            height: thickness,
3059        }
3060    } else {
3061        let x = match side {
3062            DropSide::Before => bounds.x - thickness / 2.0,
3063            DropSide::After => bounds.x + bounds.width - thickness / 2.0,
3064        };
3065
3066        Rectangle {
3067            x,
3068            y: bounds.y,
3069            width: thickness,
3070            height: bounds.height,
3071        }
3072    };
3073
3074    renderer.fill_quad(
3075        renderer::Quad {
3076            bounds: quad_bounds,
3077            border: Border {
3078                radius: 2.0.into(),
3079                ..Default::default()
3080            },
3081            shadow: Shadow::default(),
3082            snap: true,
3083        },
3084        Background::Color(color),
3085    );
3086}
3087
3088fn left_button_released(event: &Event) -> bool {
3089    matches!(
3090        event,
3091        Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left,))
3092    )
3093}
3094
3095fn right_button_released(event: &Event) -> bool {
3096    matches!(
3097        event,
3098        Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right,))
3099    )
3100}
3101
3102fn is_pressed(event: &Event) -> bool {
3103    matches!(
3104        event,
3105        Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
3106            | Event::Touch(touch::Event::FingerPressed { .. })
3107    )
3108}
3109
3110fn is_lifted(event: &Event) -> bool {
3111    matches!(
3112        event,
3113        Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left,))
3114            | Event::Touch(touch::Event::FingerLifted { .. })
3115    )
3116}
3117
3118fn touch_lifted(event: &Event) -> bool {
3119    matches!(event, Event::Touch(touch::Event::FingerLifted { .. }))
3120}