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