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