Skip to main content

cosmic/widget/segmented_button/
widget.rs

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